Это вторая часть статьи, посвященной универсальному сервису подписания для инфраструктуры Госуслуг. Первая часть статьи была посвящена GO-части нового сервиса, в которую входит конфигурирование, тестирование, ресты, файловый менеджер и планировщик. Всему тому, что происходит до того, как данные будут переданы в С-часть для подписания и верификации. Также там кратко описаны основные предпосылки к создания нового сервиса.

Приступать к чтению этой статьи рекомендуется строго после ознакомления с первой частью.

Во второй части будет рассмотрена CGO-прослойка и C-часть, в которой происходит подписание и верификация данных. Тут же будут описаны самые важные оптимизации, ради которых этот сервис и создавался.

Для сохранения времени читателей примеры кода в этой статье по возможности упрощены. Там, где это не мешает, читаемости полностью удалена инициализация переменных, переиспользование буферов, обработка ошибок и освобождение памяти.

А чтобы еще сильнее упростить дальнейшее чтение, я сделал схему, в которой есть все элементы обеих частей статьи. Те элементы, которые будут рассмотрены в этой статье, выделены красным.

CGO-часть

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

Логирование

При проектировании логера почти сразу появился вопрос, как вообще в большом, мультиязычном и многопоточном проекте можно отслеживать запросы? Как, например понять, к какому из запросов относится конкретная ошибка?

Было очевидно, что потребуется что-то, что можно передавать во все методы сервиса. А поскольку у нас к этому моменту уже достаточно активно использовался uuid, было решено остановится на нем.

Использовать uuid оказалось удобно еще и потому, что он является обязательной частью запросов, в которых данные могут быть разбиты на чанки, так как в таких запросах он используется как уникальное имя директории, в которой чанки хранятся между запросами. А если в запросе uuid передавать не требуется, как, например, при подписи или верификации OAuth токенов, то его достаточно сгенерировать и вернуть в заголовке ответа.

Благодаря такому подходу, чтобы понять, к какому запросу относится ошибка, достаточно просто поискать в логе все строки, в которых присутствует нужный uuid. Это решило проблему с многопоточностью, но не решило полностью проблему с мультиязычностью, так как логгеры С и GO-частей необходимо было еще и синхронизировать.

Поскольку задача достаточно трудоемкая, было решено делать логирование только на стороне GO, а при необходимости логирования С-части просто вызывать логер через CGO. Но так как вызовы CGO достаточно дорогие, количество info логов было решено минимизировать, а чтобы логер на стороне C лишний раз не вызывал GO-код, пришлось пробрасывать уровень логирования еще и в C-часть. Это позволило отсекать все неподходящие по уровню логирования вызовы до того, как они попадут в CGO.

Обработка ошибок

На обработке ошибок я изначально вообще не хотел останавливаться, но раз уже мы заговорили про прослойку, то почему бы не рассказать, как через нее передаются ошибки?

Для того чтобы можно было нормально пробрасывать ошибки, приходящие из C, необходимо, помимо самих кодов ошибок на стороне GO поддерживать еще и константы, аналогичные таковым в С, например:

var (
    ErrGeneral            = errors.New("general error")
)

const (
	GENERAL_ERROR         = -1
)

Благодаря этим константам код ошибки возвращаемый из С, можно конвертировать в стандартный для GO без потери изначального смысла, например:

func ConvertCError(cErr int) (err error) {
	switch cErr {
	case GENERAL_ERROR:
		return ErrGeneral
	default:
		return ErrUnexpected
	}
}

Проброс указателей

Теперь можно вернуться к описанию самой прослойки и, в частности, к пробросу указателей из GO в C и обратно.

Необходимость в пробросе указателей возникла из-за того, что иногда приходится передавать через стэк большие объемы данных для подписи или верификации.

Чтобы эти данные не копировать при вызове C-методов, было принято решение пробрасывать указатель на данные из-под слайса сразу в C-часть, как обычный C-указатель, а затем, после возврата из метода, C-указатель можно подложить под GO-слайс, тем самым тоже сэкономив дополнительную память, например:

В этом примере при вызове C-метода signXmlSmev2 в него в параметре cXmlToSign передается указатель из-под слайса xmlToSign с данными для подписи. А при возврате из signXmlSmev2 в параметре cSignedXml уже есть указатель на подписанные данные и его можно подложить под слайс signedXml который вернется из метода CGOSignXmlSmev2, например:

func CGOSignXmlSmev2(uuid string, xmlToSign []byte) (signedXml []byte, err error) {

	var cXmlToSign *C.char = nil
	var cXmlToSignLen C.int = 0

	//Освобождается из вызывающей функции вызовом c.CGOFreeCPointerUnderSlice
	var cSignedXml *C.char = nil
	var cSignedXmlLen C.int = 0

	if xmlToSign != nil && len(xmlToSign) > 0 {
		cXmlToSign = (*C.char)(unsafe.Pointer(&xmlToSign[0]))
		cXmlToSignLen = C.int(len(xmlToSign))
	}

	cRes := C.signXmlSmev2(cXmlToSign, cXmlToSignLen, &cSignedXml, &cSignedXmlLen)
	if int(cRes) != error_codes.OK {
		err := error_codes.ConvertCError(int(cRes))
		logger.Error.Println(uuid, "C.signXmlSmev2 failed:", err)
		return signedXml, err
	}

	sliceHeader := reflect.SliceHeader{Data: (uintptr)(unsafe.Pointer(cSignedXml)), Len: int(cSignedXmlLen), Cap: int(cSignedXmlLen)}
	signedXml = *(*[]byte)(unsafe.Pointer(&sliceHeader))

	return signedXml, err
}

Чтобы потом освободить указатель под слайсом достаточно его опять достать и передать в стандартный метод для освобождения, например:

func CGOFreeCPointerUnderSlice(uuid string, slice []byte) (err error) {

	C.free(unsafe.Pointer(&slice[0]))

	return nil
}

С-часть

Теперь, когда все, по моему мнению, важные моменты в GO и CGO-частях были описаны, пришло время перейти к самому интересному и самому высоконагруженному коду, который находится в C-части сервиса.

Но перед этим все-таки стоит отдельно упомянуть про xpath, так как любые xpath-операции очень дорогие и, если бы работа с ним не была оптимизирована, сервис бы работал в несколько раз медленнее.

А возможно это стало благодаря тому, что форматы xml-документов, которые поступают на вход, строго регламентированы в методических рекомендациях СМЭВ. Поэтому при подписи документа нам достаточно найти только подписываемые данные, а при верификации саму подпись и все данные по ссылкам из подписи. Остальное можно найти простыми переходами по ссылкам DOM в рамках элемента подписи.

Канонизация

Канонизация является неотъемлемой частью подписания xml, а сами данные для канонизации могут быть по размеру практически как исходный файл. В связи с чем возникла необходимость сделать канонизацию без копирования в память еще одного объема изначального файла, и в libxm2 для этого есть подходящие инструменты.

В метод для канонизации также, как и в методы для дампа и парсинга xml, описанных в первой части, можно передать буфер, в котором будет содержаться callback, куда будут приходить данные по кускам.

И если в такой callback передать контекст хэширования, то можно будет передавать в метод хеширования каждый кусок данных отдельно. Что избавляет от необходимости хранить в памяти одновременно все канонизированные данные, например.

В примере вызывается метод xmlOutputBufferCreateIO, который позволяет создать буфер, при заполнении которого будут вызываться переданные нами callback c14BufWriteCallback, в который первым параметром будет приходить переданный нами контекст bufWriteCallbackDataForSignedInfo. В нем содержится контекст хеширования hHashForSignedInfo с которым будет вызываться метод для хэширования updateHashData.

Впоследствии этот буфер передается в canonXmlNode вместе с данными для канонизации, а после - в метод xmlC14NExecute который и будет заниматься канонизацией данных.

//Инициализация структуры для передачи в callback
c14BufWriteCallbackData bufWriteCallbackDataForSignedInfo = {.uuid = uuid, .hHashHandle = hHashForSignedInfo};

//Инициализация буфера
canonBufForSignedInfo = xmlOutputBufferCreateIO(c14BufWriteCallback, NULL, &bufWriteCallbackDataForSignedInfo, NULL);
if(NULL == canonBufForSignedInfo)
{
	res = GENERAL_ERROR;
	errorLog(uuid, LOGGER_FILE_INFO, "xmlOutputBufferCreateIO failed");
	goto err;
}

//Канонизация данных для подписи
res = canonXmlNode(uuid, doc, signedInfo, canonBufForSignedInfo);
if(OK != res)
{
	errorLog(uuid, LOGGER_FILE_INFO, "canonXmlNode failed");
	goto err;
}

//Освобождение буфера
xmlRes = xmlOutputBufferClose(canonBufForSignedInfo);
if(-1 == xmlRes)
{
	res = GENERAL_ERROR;
	errorLog(uuid, LOGGER_FILE_INFO, "xmlOutputBufferClose failed");
	goto err;
}

Пример определения структуры c14BufWriteCallbackData:

typedef struct
{
		const char      *uuid;
 		HCRYPTHASH      hHashHandle;
} c14BufWriteCallbackData;

Пример c14BufWriteCallback:

//Вызывается из метода канонизации и по чанкам пишет данные в метод хэширования
static int c14BufWriteCallback(void *context, const char *buffer, int len)
{
	error_code res = OK;
	c14BufWriteCallbackData *callbackData = (c14BufWriteCallbackData*) context;

	if(len <= 0)
		return len;

	res = updateHashData(callbackData->uuid, callbackData->hHashHandle, (const BYTE*) buffer, (const DWORD) len);
	if (OK != res)
	{
		errorLog(callbackData->uuid, LOGGER_FILE_INFO, "updateHashData failed");
		return -1;
	}

	return len;
}

Пример canonXmlNode:

error_code canonXmlNode(const char *uuid, const xmlDocPtr doc, const xmlNodePtr node, xmlOutputBufferPtr buf)
{
	c14NCanonCallbackData callbackData = {.uuid = uuid, .targetNode = node, .includeOther = 0};

	int xmlRes = 0;
	error_code res = OK;

	debugLog(uuid, LOGGER_FILE_INFO, "started");

	if(NULL == node)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "bad input");
		goto err;
	}

	xmlRes = xmlC14NExecute(doc, c14NCanonCallback, &callbackData, XML_C14N_EXCLUSIVE_1_0, NULL, 0, buf);
	if (0 > xmlRes)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "xmlC14NExecute failed");
		goto err;
	}

err:

	return res;
}

У такого метода канонизации есть и обратная сторона - приходится писать алгоритм, который будет решать, какие узлы из документа нужно канонизировать, а какие нет. Этот алгоритм не тривиальный и сначала была идея приложить пример метода c14NCanonCallback, который этим занимается, но он не обязательный для понимания принципа работы, а статья и так уже получается очень большая.

Чтобы не реализовывать этот метод, можно было бы преобразовать узел, который надо канонизировать к документу и уже его канонизировать полностью. Чтобы в итоге получить все нужные данные в одном большом буфере, который вернул бы libxml2. Но в этом случае в памяти может храниться практически полный объем исходных данных, что не допустимо при подписи больших файлов.

Канонизация для СМЭВ3

В случае с подписанием для СМЭВ2 нужна только стандартная канонизация. Но в случае с подписанием для СМЭВ3 все намного сложнее, потому что после стандартной канонизации необходимо выполнить и собственную трансформацию СМЭВ.

Поскольку метод canonXmlNode идентичен тому, что описан в обычной канонизации, его код в примерах я приводить не буду.

Большая часть логики в примерах ниже аналогична описанной ранее, только вместо c14BufWriteCallback используется куда более сложный c14BufWriteCallbackWithSmevCanon. В котором не просто вызывается метод хэширования, а инициализируется SAX-парсер, в который по кускам передаются все пришедшие данные, и внутри него уже выполняется потоковая трансформация. Метод хэширования так же периодически вызывается внутри парсера, когда накапливается достаточно данных.

Из-за усложнившейся логики возникла необходимость в передаче еще и callback-а для освобождения памяти c14BufCloseCallbackWithSmevCanon, пример которого я приводить не стал.

Все вышеописанное позволяет сильно уменьшить объем данных, которые одновременно могут находиться в памяти.

В примере вызывается метод xmlOutputBufferCreateIO, который позволяет создать буфер, при заполнении которого будут вызываться переданные нами callback c14BufWriteCallbackWithSmevCanon, в который первым параметром будет приходить переданный нами контекст bufWriteCallbackDataForData.  В нем содержится контекст для парсера и метода.

Впоследствии этот буфер передается в canonXmlNode вместе с данными для канонизации, а после - в метод xmlC14NExecute который и будет заниматься канонизацией данных.

xmlOutputBufferPtr canonBufForData = NULL;

//Инициализация структуры для передачи в callback
c14BufWriteCallbackWithSmevCanonData bufWriteCallbackDataForData = {.uuid = uuid, .hHashHandle = hHashForData, .doSmevCanon = true, .ctxt = NULL, .saxUserData = NULL};

//Инициализация буфера
canonBufForData = xmlOutputBufferCreateIO(c14BufWriteCallbackWithSmevCanon, c14BufCloseCallbackWithSmevCanon, &bufWriteCallbackDataForData, NULL);
if(NULL == canonBufForData)
{
	res = GENERAL_ERROR;
	errorLog(uuid, LOGGER_FILE_INFO, "xmlOutputBufferCreateIO failed");
	goto err;
}

//Канонизация данных для подписи
res = canonXmlNode(uuid, dataDoc, dataRoot, canonBufForData);
if(OK != res)
{
	errorLog(uuid, LOGGER_FILE_INFO, "canonXmlNode failed");
	goto err;
}

//Освобождение буфера
xmlRes = xmlOutputBufferClose(canonBufForData);
if(-1 == xmlRes)
{
	res = GENERAL_ERROR;
	errorLog(uuid, LOGGER_FILE_INFO, "xmlOutputBufferClose failed");
	goto err;
}

canonBufForData = NULL;

Пример определения структуры c14BufWriteCallbackWithSmevCanonData:

typedef struct
{
    const char        *uuid;
    const HCRYPTHASH  hHashHandle;
    const bool        doSmevCanon;
    xmlParserCtxtPtr  ctxt;
    saxCallbackData   *saxUserData;
} c14BufWriteCallbackWithSmevCanonData;

Пример c14BufWriteCallbackWithSmevCanon:

static int c14BufWriteCallbackWithSmevCanon(void *context, const char *buffer, int len)
{
	error_code res = OK;
	c14BufWriteCallbackWithSmevCanonData *callbackData = (c14BufWriteCallbackWithSmevCanonData*) context;

	if(!callbackData->doSmevCanon)
	{
		if(len <= 0)
			return len;

		res = updateHashData(callbackData->uuid, callbackData->hHashHandle, (const BYTE*) buffer, (const DWORD) len);
		if (OK != res)
		{
			errorLog(callbackData->uuid, LOGGER_FILE_INFO, "updateHashData failed");
			return -1;
		}

		return len;
	}

	if(len <= 0)
	{
		res = cleanupSmevTransform(callbackData->uuid, callbackData->ctxt, buffer, len);
		if(OK != res)
		{
			errorLog(callbackData->uuid, LOGGER_FILE_INFO, "cleanupSmevTransform failed");
			return -1;
		}

		if(OK != callbackData->saxUserData->err)
		{
			errorLog(callbackData->uuid, LOGGER_FILE_INFO, "SAX parser failed");
			return -1;
		}

		return len;
	}

	if(callbackData->ctxt == NULL)
	{
		//Нужно для инициализации констант
		saxCallbackData tempSaxUserData = {
			.uuid = callbackData->uuid,
			.hHashHandle = callbackData->hHashHandle,
			.hashFuncPtr = &updateHashData,
			.prefixMappingStack = NULL,
			.nsCounter = 1,
			.err = OK,
			.tempBuff = NULL,
			.tempBuffLen = 0,
			.tempBuffFreeSpace = 0
		};

		//Чтобы не освободилась после выхода из функции
		saxCallbackData *saxUserData = (saxCallbackData*) malloc(sizeof(saxCallbackData));
		if(NULL == saxUserData)
		{
			errorLog(callbackData->uuid, LOGGER_FILE_INFO, "malloc failed");
			return -1;
		}

		//Копируем структуру из стэка в выделенную память
		memcpy(saxUserData, &tempSaxUserData, sizeof(saxCallbackData));

		//Выделяем сразу буфер размером с первый кусок данных после конанизицииканонизации
		saxUserData->tempBuff = (char *) malloc( (sizeof(char) * len) + SMEV_CANON_BUF_ADDITION );
		if(NULL == saxUserData->tempBuff)
		{
			errorLog(callbackData->uuid, LOGGER_FILE_INFO, "malloc failed");
			return -1;
		}

		saxUserData->tempBuffLen = 0;
		saxUserData->tempBuffFreeSpace = (sizeof(char) * len) + SMEV_CANON_BUF_ADDITION;

		//Копируем ссылку на структуру в памяти
		callbackData->saxUserData = saxUserData;

		res = initSmevTransform(callbackData->uuid, callbackData->saxUserData, buffer, len, &callbackData->ctxt);
		if(OK != res)
		{
			errorLog(callbackData->uuid, LOGGER_FILE_INFO, "initSmevTransform failed");
			return -1;
		}

		if(OK != callbackData->saxUserData->err)
		{
			errorLog(callbackData->uuid, LOGGER_FILE_INFO, "SAX parser failed");
			return -1;
		}

		return len;
	}

	res = updateSmevTransform(callbackData->uuid, callbackData->ctxt, buffer, len);
	if(OK != res)
	{
		errorLog(callbackData->uuid, LOGGER_FILE_INFO, "updateSmevTransform failed");
		return -1;
	}

	if(OK != callbackData->saxUserData->err)
	{
		errorLog(callbackData->uuid, LOGGER_FILE_INFO, "SAX parser failed");
		return -1;
	}

	return len;
}

Сама реализация трансформации через SAX занимает более 1000 строк кода и приводить ее пример здесь смысла нет.

Но для лучшего понимания работы алгоритма ниже приведены примеры методов initSmevTransform, updateSmevTransform, cleanupSmevTransform, которые вызываются из c14BufWriteCallbackWithSmevCanon.

Они, как понятно из названий, занимаются инициализацией контекста SAX-парсера и добавлением в него новых данных.

Пример initSmevTransform:

error_code initSmevTransform(const char *uuid, saxCallbackData *callbackData, const char* firstChunk, int firstChunkLen, xmlParserCtxtPtr *ctxt)
{
	xmlSAXHandler saxHandlers = {
		.initialized = XML_SAX2_MAGIC,
		.startDocument = saxStartDocument,
		.endDocument = saxEndDocument,
		.startElementNs = saxStartElementNs,
		.endElementNs = saxEndElementNs,
		.characters = saxCharacters,
		.warning = saxWarning,
		.error = saxError,
		.fatalError = saxFatalError
	};

	char chars[4];

	error_code res = OK;

	debugLog(uuid, LOGGER_FILE_INFO, "started");

	if(NULL == callbackData || NULL == firstChunk || 0 >= firstChunkLen || NULL == ctxt)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "bad input");
		goto err;
	}

	memcpy(chars, firstChunk, sizeof(chars));

	*ctxt = xmlCreatePushParserCtxt(&saxHandlers, callbackData, chars, sizeof(chars), NULL);
	if (NULL == *ctxt) {
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "xmlCreatePushParserCtxt failed");
		goto err;
	}

	res = xmlCtxtUseOptions(*ctxt, XML_PARSE_HUGE);
	if(OK != res)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "xmlCtxtUseOptions failed");
		goto err;
	}

	res = xmlCtxtUseOptions(*ctxt, XML_PARSE_NOENT);
	if(OK != res)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "xmlCtxtUseOptions failed");
		goto err;
	}

	res = xmlParseChunk(*ctxt, firstChunk + sizeof(chars), firstChunkLen - sizeof(chars), 0);
	if(OK != res)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed");
		goto err;
	}
  
	if (1 != (*ctxt)->wellFormed || 0 != (*ctxt)->errNo)
	{
		res = XML_PARSE_ERROR;
		formatErrorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed, wellFormed: %d, err: %d", (*ctxt)->wellFormed, (*ctxt)->errNo);
		goto err;
	}

err:

	return res;
}

Пример updateSmevTransform:

error_code updateSmevTransform(const char *uuid, xmlParserCtxtPtr ctxt, const char* chunk, int chunkLen)
{
	error_code res = OK;

	debugLog(uuid, LOGGER_FILE_INFO, "started");

	if(NULL == ctxt || NULL == chunk || 0 >= chunkLen)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "bad input");
		goto err;
	}

	res = xmlParseChunk(ctxt, chunk, chunkLen, 0);
	if(OK != res)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed");
		goto err;
	}
  
	if (1 != ctxt->wellFormed || 0 != ctxt->errNo)
	{
		res = XML_PARSE_ERROR;
		formatErrorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed, wellFormed: %d, err: %d", ctxt->wellFormed, ctxt->errNo);
		goto err;
	}

err:

	return res;
}

Пример cleanupSmevTransform:

error_code cleanupSmevTransform(const char *uuid, xmlParserCtxtPtr ctxt, const char* lastChunk, int lastChunkLen)
{
	error_code res = OK;

	debugLog(uuid, LOGGER_FILE_INFO, "started");

	if(NULL == ctxt)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "bad input");
		goto err;
	}

	res = xmlParseChunk(ctxt, lastChunk, lastChunkLen, 1);
	if(OK != res)
	{
		res = GENERAL_ERROR;
		errorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed");
		goto err;
	}
  
	if (1 != ctxt->wellFormed || 0 != ctxt->errNo)
	{
		res = XML_PARSE_ERROR;
		formatErrorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed, wellFormed: %d, err: %d", ctxt->wellFormed, ctxt->errNo);
		goto err;
	}

err:

	if(NULL != ctxt)
		xmlFreeParserCtxt(ctxt);

	return res;
}

Подпись файлов по чанкам

В финальной части хотелось бы поговорить про подпись и верификацию файлов.

Хоть это и не относится к рестам, которые мы описывали, тут тоже есть, о чем рассказать. Например, создавать и верифицировать подписи для файлов тоже можно по чанкам. А так как сервис не накладывает никаких ограничений на их размер, и в реальных условиях размер файла запросто может превышать гигабайт, это может сэкономить очень много ресурсов.

Для этих целей лучше всего воспользоваться возможностями реализации CAPILite от КриптоПро для *nix систем, в которой можно для стандартов CMS и CAdES-BES создавать и верифицировать подписи по чанкам. А поскольку операции для подписания и верификации на этом уровне не сильно отличаются, я приведу пример только для операций подписания.

В примере ниже сначала вызывается метод CryptMsgOpenToEncode из capilite для создания контекста сообщения, в который, с помощью метода CryptMsgUpdate можно помещать данные по чанкам, читая их, например, из файлового менеджера. А в конце вызывается метод CryptMsgGetParam, который позволяет получить подпись из контекста, когда все необходимые данные туда уже были добавлены.

HCRYPTMSG       hMsg = 0;

BYTE * signatureBlob = NULL;
DWORD signatureBlobLen = 0;

//Создадим дескриптор сообщения
hMsg = CryptMsgOpenToEncode(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, flags, CMSG_SIGNED, &SignedMsgEncodeInfo, NULL, NULL);
if(NULL == hMsg)
{
	localRes = false;
	printCapiError(uuid, LOGGER_FILE_INFO, "CryptMsgOpenToEncode failed");
	goto err;
}

//Поместим в сообщение подписываемые данные
localRes = CryptMsgUpdate(hMsg, data, dataLen, TRUE);
if(true != localRes)
{
	printCapiError(uuid, LOGGER_FILE_INFO, "CryptMsgUpdate failed");
	goto err;
}

//Определим длину подписанного сообщения
localRes = CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, NULL, &signatureBlobLen);
if (true != localRes)
{
	DWORD err = GetLastError();
	SetLastError(err);

	if(err == SCARD_W_WRONG_CHV)
	{
		res = INVALID_PIN;
		printCapiError(uuid, LOGGER_FILE_INFO, "CryptMsgGetParam failed, invalid pin");
	}
	else if(err == NTE_PERM)
	{
		res = CERT_EXPIRED;
		printCapiError(uuid, LOGGER_FILE_INFO, "CryptMsgGetParam failed, access denied, most likely certificate expired");
	}
	else
	{
		printCapiError(uuid, LOGGER_FILE_INFO, "CryptMsgGetParam failed");
	}

	goto err;
}

//Резервируем память
signatureBlob = (BYTE *) malloc((size_t) signatureBlobLen);
if(NULL == signatureBlob)
{
	localRes = false;
	errorLog(uuid, LOGGER_FILE_INFO, "malloc failed");
	goto err;
}

//Вернем подписанное сообщение
localRes = CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, signatureBlob, &signatureBlobLen);
if (true != localRes)
{
	DWORD err = GetLastError();
	SetLastError(err);

	if(err == SCARD_W_WRONG_CHV)
	{
		res = INVALID_PIN;
		printCapiError(uuid, LOGGER_FILE_INFO, "CryptMsgGetParam failed, invalid pin");
	}
	else if(err == NTE_PERM)
	{
		res = CERT_EXPIRED;
		printCapiError(uuid, LOGGER_FILE_INFO, "CryptMsgGetParam failed, access denied, most likely certificate expired");
	}
	else
	{
		printCapiError(uuid, LOGGER_FILE_INFO, "CryptMsgGetParam failed");
	}

	goto err;
}

if(hMsg)
	CryptMsgClose(hMsg);

if(signatureBlob)
	free(signatureBlob);

В процессе редактирования статьи уже практически была готова собственная реализация стандартов CMS и CAdES-BES для подписи файлов, так как появились задачи, которые нельзя решить только с помощью КриптоПро.

Если будет интересно, могу написать об этом в отдельной статье.

Заключение

В заключение хотелось бы добавить, что тут описан далеко не полный список архитектурных решений и оптимизаций, благодаря которым сервис получился быстрее предыдущего. Я постарался описать только самые важные из них, так что вполне возможно, что не у всех сложилась полная картина.

Я также решил не перегружать и так большую статью еще и информацией о профайлинге, сборке и развертыванию, так как тут все без велосипедов. Да и по всем упомянутым темам уже существует много отдельных постов и хорошая документация. Если совсем кратко, то для профайлинга был выбран pprof, как для C, так и GO-частей, сборкой управляет Makefile, зависимостями управляет dep (идет переход на go modules), а сам сервис разворачивается в докере.

Спасибо за внимание, и за то, что смогли дочитать статью до конца.

Если у вас есть замечания или предложения по дальнейшему улучшению кода или идеи для еще одной статьи, с удовольствием почитаю об этом в комментариях.