Ориентация главы

В TLS две стороны «доказывают себя»: клиент доказывает «кому я доверяю» (верификатор, глава 85), сервер доказывает «кто я» (идентичность, эта глава). xtlsidentity упаковывает одну DER-цепочку сертификатов и один приватный ключ в неизменяемый разделяемый объект: при создании глубоко копирует весь материал и выполняет перекрёстную проверку (действительно ли приватный ключ соответствует публичному в сертификате? какие схемы TLS эта цепочка может подписывать?), после чего объект конкурентно разделяется потоками и конфигурациями серверов, а рукопожатие пользуется им для производства подписи CertificateVerify. Четыре строго типизированных конструктора (RSA/P-256/P-384/Ed25519) покрывают магистральные серверы; расширительный интерфейс xrtTlsIdentityCreate отдаёт подписную способность на аутсорс HSM, системному хранилищу ключей или удалённому подписчику. Аутентификация клиентскими сертификатами сейчас не реализована (контракт главы 82), поэтому идентичность сегодня — понятие серверное; сам объект от роли отвязан.

83.1引入

Что готовят для развёртывания TLS-сервера? Цепочку сертификатов (листовой + промежуточные CA, PEM-файл) и приватный ключ. Неприятности начинаются с загрузки: DER-форматов приватного ключа несколько (PKCS#1, PKCS#8, голый скаляр SEC1, семя Ed25519); алгоритм публичного ключа сертификата должен совпасть с приватным; у RSA-PSS-сертификатов ещё и параметрические ограничения (дайджест, MGF1, нижний предел соли) — при любом несовпадении подпись провалится только в рукопожатии, и встречать вы будете тревогу трёх часов ночи, а не ясную ошибку при старте.

Ответ xtlsidentity — полная проверка на этапе создания: конструктор разбирает приватный ключ, сверяет SPKI сертификата, проверяет общие подписные схемы для версий TLS — «у идентичности нет ни одной общей TLS-схемы — отказ прямо при конструировании». Упасть при старте в десять тысяч раз лучше, чем в рукопожатии: опыт развёртывания и опыт безопасности — одно и то же. Прошедшая проверку идентичность неизменяема: цепочка сертификатов глубоко копируется в один компактный блок (перед освобождением безопасно обнуляется), заимствованные представления действуют до освобождения последней ссылки — много Worker-ов разделяют один объект, ноль блокировок.

83.2概念

Четыре конструктора и их диалекты приватных ключей

КонструкторВходная форма приватного ключаПроверка на этапе создания
xrtTlsIdentityRsaPKCS#1 или незашифрованный PKCS#8 DERмодуль, экспонента, все CRT-параметры, листовой SPKI, ограничения RSA-PSS
xrtTlsIdentityP25632-байтовый скаляр / SEC1 / незашифрованный PKCS#8диапазон скаляра, OID кривой, опциональный публичный ключ SEC1, листовой P-256 SPKI
xrtTlsIdentityP38448-байтовый скаляр / SEC1 / незашифрованный PKCS#8то же (P-384)
xrtTlsIdentityEd2551932-байтовое семя / одно-двухслойный DER OCTET / RFC 8410 PKCS#8умолчания параметров алгоритма, производный публичный ключ, листовой Ed25519 SPKI

Три общности: первый сертификат цепочки обязан быть листовым; приватный ключ не заимствуется (глубоко копируется в идентичность; буфер вызывающего можно переиспользовать немедленно); RSA сохраняет полную CRT-вид, Ed25519 — однократно развёрнутый подписной ключ — путь рукопожатия не повторяет деривацию. PEM/файл/системное хранилище не входят в ядро идентичности: сперва возьмите DER через PEM-декодирование главы 78, затем отдайте конструктору — между слоями ходит только DER.

CanSign: тройной запрос способности

xrtTlsIdentityCanSign(身份, 版本, 方案) отвечает «может ли эта идентичность подписать этой схемой» — проверяя одновременно ограничения версии TLS, тип идентичности сертификата, стандартные схемы и влитые бэкенды. Суть правил: TLS 1.3 не принимает RSA-PKCS#1 в CertificateVerify (жёсткое правило современного протокола); схемы ECDSA обязаны совпадать с кривой сертификата; сертификаты rsaEncryption и RSASSA-PSS входят только в пути rsa_pss_rsae_* и rsa_pss_pss_* соответственно; у ограниченного PSS-ключа параметры обязаны одновременно удовлетворять обеим сторонам — сертификату и приватному ключу. Белый список подписей политики (глава 85) пересекается с CanSign — это и есть фактическая схема рукопожатия; exampleSignature образца главы демонстрирует выбор ходовой схемы по типу идентичности.

Sign: двухшаговая подпись и определённость

xrtTlsIdentitySign(身份, 版本, 方案, 内容, 输出, 容量, &长度) принимает полное TLS-содержимое под подпись (протокольный контекстный заголовок строит вызывающий — 64-пробельный ролевой префикс TLS 1.3 обрабатывается на уровне открытого примитива xrtTls13CertificateVerifySignature). Двухшаговость по обычаю всей библиотеки: пустой выход запрашивает точную длину, нехватка ёмкости не зовёт подписчика, неудача не публикует частичной подписи. Стратегии случайности трёх бэкендов: RSA-PSS берёт криптографически безопасную случайную соль; ECDSA — детерминированная подпись RFC 6979 low-S (глава 76 объясняла, почему определённость — достоинство); чистый режим Ed25519 определён сам по себе.

Create: аппаратура и удалённые подписчики

В реальной корпоративной среде приватный ключ часто живёт в HSM или системном хранилище ключей и не экспортируется в DER. xrtTlsIdentityCreate — расширительный интерфейс: цепочка сертификатов всё так же глубоко копируется XRT, подписная способность отдаётся на аутсорс колбэкам Supports/Sign — оба обязаны допускать конкурентные вызовы; Supports — предикат способности без ошибки, Sign при неудаче не меняет выход. Без аппаратных потребностей берите встроенные строго типизированные конструкторы — колбэчный порт приготовлен для втыкаемости, это не ежедневный путь.

Жизненный цикл: от загрузки к разделению

1ЗагрузкаPEM/файл -> DER (глава 78), цепочка сертификатов + приватный ключ уходят конструктору
2Проверкаразбор приватного ключа + перекрёстная сверка SPKI + проверка общих схем (неудача - отказ)
3共享不可变对象被服务端配置/多 Worker 并发引用(Retain/Release)
4Уничтожениепоследнийссылка освобождён -> весь ключевой материал безопасно обнулено -> колбэк Release ровно один раз

Retain/Release — подсчёт ссылок; при освобождении последней весь ключевой материал безопасно обнуляется и вызывается колбэк Release (форма Create). Неизменяемость идентичности значит: разделяемость конфигурациями многих серверов, конкурентная подпись многими Worker-ами, комбинирование с политиками/контекстами — собрали раз, разделяем везде; та же философия «разделяемое только-чтение дешевле всего», что у пула объектов главы 22, но здесь разделяется неизменяемое значение, а не переиспользуемый слот.

Retain/Release — тот же подсчёт ссылок; последнее освобождение обнуляет материал и зовёт колбэк Release — см. диаграмму выше.

83.3示例

Первая полная программа: конструкция четырёх форм ключей и запрос типа

Программа ниже из examples/tls/identity/main.c; без параметров самопроверяет четыре конструктора идентичности на встроенном материале:

examples/tls/identity/main.c源码 ↗
#include "../common.h"
#include "../embedded_identity.h"



/* 选择每种内置身份在 TLS 1.3 中的常用签名方案。 */
static xtlssignature exampleSignature(const xtlsidentity* pIdentity)
{
	switch ( xrtTlsIdentityType(pIdentity) ) {
		case XTLS_IDENTITY_RSA:
			return XTLS_SIGNATURE_RSA_PSS_RSAE_SHA256;

		case XTLS_IDENTITY_RSA_PSS:
			return XTLS_SIGNATURE_RSA_PSS_PSS_SHA256;

		case XTLS_IDENTITY_ECDSA_P256:
			return XTLS_SIGNATURE_ECDSA_SECP256R1_SHA256;

		case XTLS_IDENTITY_ECDSA_P384:
			return XTLS_SIGNATURE_ECDSA_SECP384R1_SHA384;

		case XTLS_IDENTITY_ED25519:
			return XTLS_SIGNATURE_ED25519;

		default:
			return (xtlssignature)0;
	}
}























 */


/* 无参数时用嵌入材料自检四种密钥形态构造器(材料见 embedded_identity.h)。 */
static int exampleEmbeddedCheck(void)
{
	struct {
		const uint8* pCertificate;
		size_t iCertificateSize;
		const uint8* pPrivateKey;
		size_t iPrivateKeySize;
		cstr sName;
	} arrForms[4];
	xtlsidentity* pIdentity;
	size_t i;

	arrForms[0].pCertificate = g_ExampleRsaCertDer;
	arrForms[0].iCertificateSize = sizeof(g_ExampleRsaCertDer);
	arrForms[0].pPrivateKey = g_ExampleRsaKeyDer;
	arrForms[0].iPrivateKeySize = sizeof(g_ExampleRsaKeyDer);
	arrForms[0].sName = "rsa";
	arrForms[1].pCertificate = g_ExampleCertDer;
	arrForms[1].iCertificateSize = sizeof(g_ExampleCertDer);
	arrForms[1].pPrivateKey = g_ExampleKeyDer;
	arrForms[1].iPrivateKeySize = sizeof(g_ExampleKeyDer);
	arrForms[1].sName = "p256";
	arrForms[2].pCertificate = g_ExampleP384CertDer;
	arrForms[2].iCertificateSize = sizeof(g_ExampleP384CertDer);
	arrForms[2].pPrivateKey = g_ExampleP384KeyDer;
	arrForms[2].iPrivateKeySize = sizeof(g_ExampleP384KeyDer);
	arrForms[2].sName = "p384";
	arrForms[3].pCertificate = g_ExampleEd25519CertDer;
	arrForms[3].iCertificateSize = sizeof(g_ExampleEd25519CertDer);
	arrForms[3].pPrivateKey = g_ExampleEd25519KeyDer;
	arrForms[3].iPrivateKeySize = sizeof(g_ExampleEd25519KeyDer);
	arrForms[3].sName = "ed25519";

	for ( i = 0; i < 4u; i++ ) {
		const xbytesview arrCertificates[1] = {
			(xbytesview){ arrForms[i].pCertificate, arrForms[i].iCertificateSize }
		};
		xbytesview PrivateKey = { arrForms[i].pPrivateKey, arrForms[i].iPrivateKeySize };

		if ( strcmp(arrForms[i].sName, "rsa") == 0 ) {
			pIdentity = xrtTlsIdentityRsa(arrCertificates, 1u, PrivateKey);
		} else if ( strcmp(arrForms[i].sName, "p256") == 0 ) {
			pIdentity = xrtTlsIdentityP256(arrCertificates, 1u, PrivateKey);
		} else if ( strcmp(arrForms[i].sName, "p384") == 0 ) {
			pIdentity = xrtTlsIdentityP384(arrCertificates, 1u, PrivateKey);
		} else {
			pIdentity = xrtTlsIdentityEd25519(arrCertificates, 1u, PrivateKey);
		}
		if ( (pIdentity == NULL)
			|| (xrtTlsIdentityCertificateCount(pIdentity) != 1u) ) {
			fprintf(stderr, "embedded %s identity failed\n", arrForms[i].sName);
			return 1;
		}
		printf("embedded %s identity=%d certificates=1\n",
			arrForms[i].sName, (int)xrtTlsIdentityType(pIdentity));
		xrtTlsIdentityRelease(pIdentity);
	}
	return 0;
}



/* 从 DER 证书与私钥创建身份,并演示精确长度查询和实际签名。 */
int main(int argc, char** argv)
{
	static const uint8 Message[] = "xrt TLS identity example";
	uint8* pCertificateData = NULL;
	uint8* pPrivateData = NULL;
	uint8* pSignature = NULL;
	size_t iCertificateSize = 0;
	size_t iPrivateSize = 0;
	size_t iSignatureSize = 0;
	xbytesview Certificate;
	xtlsidentity* pIdentity = NULL;
	xtlssignature Signature;
	int iResult = 1;

	if ( argc == 1 ) {
		return exampleEmbeddedCheck();
	}
	if ( argc != 4 ) {
		printf("usage: identity <rsa|p256|p384|ed25519> <certificate.der> <private.der>\n");
		printf("       identity            (无参数:嵌入材料自检四种形态)\n");
		return 0;
	}
	if ( !exampleTlsReadFile(
		argv[2], &pCertificateData, &iCertificateSize
	) || !exampleTlsReadFile(
		argv[3], &pPrivateData, &iPrivateSize
	) ) {
		fprintf(stderr, "failed to read DER input\n");
		goto Cleanup;
	}
	Certificate = (xbytesview) { pCertificateData, iCertificateSize };
	pIdentity = exampleTlsIdentity(
		argv[1], &Certificate, 1u,
		(xbytesview) { pPrivateData, iPrivateSize }
	);
	if ( pIdentity == NULL ) {
		fprintf(stderr, "failed to create TLS identity\n");
		goto Cleanup;
	}
	Signature = exampleSignature(pIdentity);
	if ( (Signature == 0) || !xrtTlsIdentitySign(
		pIdentity, XTLS_VERSION_13, Signature,
		(xbytesview) { Message, sizeof(Message) - 1u },
		NULL, 0, &iSignatureSize
	) ) {
		fprintf(stderr, "identity cannot sign with the selected TLS scheme\n");
		goto Cleanup;
	}
	pSignature = (uint8*)malloc(iSignatureSize);
	if ( (pSignature == NULL) || !xrtTlsIdentitySign(
		pIdentity, XTLS_VERSION_13, Signature,
		(xbytesview) { Message, sizeof(Message) - 1u },
		pSignature, iSignatureSize, &iSignatureSize
	) ) {
		fprintf(stderr, "TLS identity signing failed\n");
		goto Cleanup;
	}
	printf(
		"identity=%d certificates=%zu signature=%zu bytes\n",
		(int)xrtTlsIdentityType(pIdentity),
		xrtTlsIdentityCertificateCount(pIdentity), iSignatureSize
	);
	iResult = 0;

Cleanup:
	free(pSignature);
	xrtTlsIdentityRelease(pIdentity);
	free(pPrivateData);
	free(pCertificateData);
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/identity/main.c -lws2_32 -liphlpapiembedded rsa identity=1 certificates=1embedded p256 identity=3 certificates=1embedded p384 identity=4 certificates=1embedded ed25519 identity=5 certificates=1

Что только что произошло. (1) Четыре формы — каждая своим конструктором: RSA (1), P-256 (3), P-384 (4), Ed25519 (5) — значения перечисления IdentityType соответствуют xtlsidentitytype, стабильны кроссплатформенно. (2) Каждая конструкция и есть полная проверка: разбор приватного ключа, перекрёстная сверка SPKI, проверка общих схем — встроенный материал весь законен, конструкция проходит; подсуньте пару с несовпадающими сертификатом и ключом — конструкция падает сразу со структурной ошибкой (код ошибки identity + цепочка причин DER/X.509/crypto). (3) CertificateCount подтверждает длину цепи (здесь по одному листу на форму); реальное развёртывание передаёт полный массив промежуточных CA. (4) С параметрами программа переключается на полный путь «прочитать DER-файлы → построить → двухшаговый Sign» — exampleSignature выбирает схему по типу (RSA через PSS, Ed25519 напрямую), пустой выход запрашивает длину, затем выделение и подпись, печать числа байтов подписи (у Ed25519 всегда 64, у RSA — длина модуля). (5) Каждая идентичность после использования — Release: управлением жизненным циклом неизменяемого разделяемого объекта ведает только эта пара вызовов.

Вторая полная программа: нижний дом подписных схем

Вторая программа из examples/crypto/sign_tour/main.c — демонстрация нижнего семейства схем, на которые опирается подпись идентичности: три домена Ed25519 и DER-представление ECDSA:

examples/crypto/sign_tour/main.c源码 ↗

























 */

#include <stdio.h>
#include <string.h>
#include <xrt.h>

int main(void)
{
	static const uint8 arrSeed[32] = {
		1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u,
		9u, 10u, 11u, 12u, 13u, 14u, 15u, 16u,
		17u, 18u, 19u, 20u, 21u, 22u, 23u, 24u,
		25u, 26u, 27u, 28u, 29u, 30u, 31u, 32u
	};
	static const uint8 arrPriv256[32] = {
		0xA1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u,
		9u, 10u, 11u, 12u, 13u, 14u, 15u, 16u,
		17u, 18u, 19u, 20u, 21u, 22u, 23u, 24u,
		25u, 26u, 27u, 28u, 29u, 30u, 31u, 0xB0u
	};
	static const uint8 arrPriv384[48] = {
		0xC1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u,
		9u, 10u, 11u, 12u, 13u, 14u, 15u, 16u,
		17u, 18u, 19u, 20u, 21u, 22u, 23u, 24u,
		25u, 26u, 27u, 28u, 29u, 30u, 31u, 32u,
		33u, 34u, 35u, 36u, 37u, 38u, 39u, 40u,
		41u, 42u, 43u, 44u, 45u, 46u, 47u, 0xD0u
	};
	static const uint8 arrContext[4] = { 0x63u, 0x74u, 0x78u, 0x31u };
	uint8 arrMessage[13] = { 0x70u, 0x61u, 0x79u, 0x6Cu, 0x6Fu, 0x61u, 0x64u, 0x2Du, 0x62u, 0x79u, 0x74u, 0x65u, 0x73u };
	uint8 arrPublic[64];
	uint8 arrSignature[96];
	uint8 arrSignature2[128]; /* 第二次 DER 编码输出(约 102 字节)。 */
	uint8 arrDer[128];
	uint8 arrRaw2[96];
	uint8 arrDigest[64];
	uint8 arrPub256[65];
	uint8 arrPub384[97];
	size_t iDerSize = 0;
	xed25519key Key;
	int iResult = 1;

	/* ---- Ed25519 纯消息:种子签 → 公钥验。 ---- */
	if ( !xrtEd25519Public(arrSeed, arrPublic) ||
		!xrtEd25519Sign(arrSeed, arrMessage, sizeof(arrMessage) - 1u,
			arrSignature) ||
		!xrtEd25519Verify(arrPublic, arrMessage,
			sizeof(arrMessage) - 1u, arrSignature) ) {
		goto Cleanup;
	}
	arrSignature[0] ^= 1u;
	if ( xrtEd25519Verify(arrPublic, arrMessage,
			sizeof(arrMessage) - 1u, arrSignature) ) {
		goto Cleanup;
	}
	printf("sign: ed25519 pure sign->verify ok\n");

	/* ---- Ed25519 CONTEXT / PREHASH 域:展开密钥一次复用。 ---- */
	if ( !xrtEd25519KeyInit(&Key, arrSeed) ||
		!xrtEd25519SignMode(&Key, XED25519_CONTEXT, arrContext,
			sizeof(arrContext), arrMessage,
			sizeof(arrMessage) - 1u, arrSignature) ||
		!xrtEd25519VerifyMode(arrPublic, XED25519_CONTEXT,
			arrContext, sizeof(arrContext), arrMessage,
			sizeof(arrMessage) - 1u, arrSignature) ) {
		goto Cleanup;
	}
	/* PREHASH:消息须先取 64 字节 SHA-512。 */
	if ( !xrtSha512(arrMessage, sizeof(arrMessage) - 1u, arrDigest) ||
		!xrtEd25519SignMode(&Key, XED25519_PREHASH, NULL, 0u,
			arrDigest, XRT_ED25519_PREHASH_SIZE, arrSignature) ||
		!xrtEd25519VerifyMode(arrPublic, XED25519_PREHASH, NULL, 0u,
			arrDigest, XRT_ED25519_PREHASH_SIZE, arrSignature) ) {
		goto Cleanup;
	}
	xrtEd25519KeyClear(&Key);
	printf("sign: ed25519 context + prehash modes ok\n");

	/* ---- ECDSA-P256:签摘要 → raw 验证 → DER 验证。 ---- */
	if ( !xrtSha256(arrMessage, sizeof(arrMessage) - 1u, arrDigest) ||
		!xrtP256Public(arrPriv256, arrPub256) ||
		!xrtEcdsaP256Sign(XCRYPTO_HASH_SHA256, arrDigest, arrPriv256,
			arrSignature) ||
		!xrtEcdsaP256Verify(arrDigest, XRT_SHA256_SIZE, arrSignature,
			arrPub256) ) {
		goto Cleanup;
	}
	/* DerEncode:空输出先查询所需长度,再实际编码。 */
	if ( !xrtEcdsaDerEncode(arrSignature, 32u, NULL, 0u, &iDerSize) ||
		(iDerSize == 0u) ||
		(iDerSize > sizeof(arrDer)) ||
		!xrtEcdsaDerEncode(arrSignature, 32u, arrDer, iDerSize,
			&iDerSize) ||
		!xrtEcdsaP256VerifyDer(arrDigest, XRT_SHA256_SIZE, arrDer,
			iDerSize, arrPub256) ) {
		goto Cleanup;
	}
	printf("sign: ecdsa-p256 sign->verify(der) roundtrip ok\n");

	/* ---- ECDSA-P384:SHA-384 摘要同构闭环。 ---- */
	if ( !xrtSha384(arrMessage, sizeof(arrMessage) - 1u, arrDigest) ||
		!xrtP384Public(arrPriv384, arrPub384) ||
		!xrtEcdsaP384Sign(XCRYPTO_HASH_SHA384, arrDigest, arrPriv384,
			arrSignature) ||
		!xrtEcdsaP384Verify(arrDigest, XRT_SHA384_SIZE, arrSignature,
			arrPub384) ||
		!xrtEcdsaDerEncode(arrSignature, 48u, arrDer, sizeof(arrDer),
			&iDerSize) ||
		!xrtEcdsaP384VerifyDer(arrDigest, XRT_SHA384_SIZE, arrDer,
			iDerSize, arrPub384) ) {
		goto Cleanup;
	}
	printf("sign: ecdsa-p384 sign->verify(der) roundtrip ok\n");

	/* ---- DER 往返:DerDecode(Encode(x)) == x(P-384 签名)。 ---- */
	if ( !xrtEcdsaDerDecode(arrDer, iDerSize, arrRaw2, 48u) ||
		(memcmp(arrRaw2, arrSignature, 96u) != 0) ||
		/* DER(约 102 字节)比 96 字节 raw 更长——独立缓冲。 */
		!xrtEcdsaDerEncode(arrRaw2, 48u, arrSignature2,
			sizeof(arrDer), &iDerSize) ||
		!xrtEcdsaDerDecode(arrSignature2, iDerSize, arrRaw2, 48u) ||
		(memcmp(arrRaw2, arrSignature, 96u) != 0) ) {
		goto Cleanup;
	}
	printf("sign: der r||s <-> der roundtrip ok\n");
	iResult = 0;

Cleanup:
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/crypto/sign_tour/main.c -lws2_32 -liphlpapisign: ed25519 pure sign->verify oksign: ed25519 context + prehash modes oksign: ecdsa-p256 sign->verify(der) roundtrip oksign: ecdsa-p384 sign->verify(der) roundtrip oksign: der r||s <-> der roundtrip ok

Что только что произошло. (1) Три домена Ed25519 взаимно несовместимы: PURE (подпись сообщения напрямую), CONTEXT (со контекстной строкой — именно эта идея разделения доменов работает в CertificateVerify TLS 1.3), PREHASH (подпись 64-байтового предхеша SHA-512). Подпись междоменно непроверяема — это криптомеханизм «один алгоритм, разные назначения не смешиваются»; ролевой префикс TLS («TLS 1.3, server CertificateVerify») и защищает от переноса TLS-подписи сайта в другой протокол. (2) ECDSA работает по интерфейсу «принимает дайджест, а не сообщение» (алгоритм дайджеста объявляет перечисление Hash); raw r||s фиксированной ширины и проволочный DER взаимоконвертируемы — круговорот двойного представления ECDSA из главы 76. (3) Сверьте со слоем идентичности: внутри IdentitySign ровно эти нижние примитивы плюс TLS-контекстная обёртка — понимание нижнего слоя помогает диагностировать межслойные вопросы вида «CanSign разрешил, а рукопожатие падает» (обычно белый список политики не открыл схему).

83.4契约

  • Неизменяемое разделение: после создания неизменяем; разделение подсчётом ссылок; заимствованные представления (сертификаты/публичные ключи) действуют до освобождения последней ссылки; многопоточная подпись без блокировок.
  • Проверка на этапе создания: разбор приватного ключа + перекрёстная сверка листового SPKI + проверка общих схем TLS; нет общей схемы — прямой отказ; первый сертификат цепи обязан быть листовым.
  • Глубокая копия приватного ключа: конструкторы не заимствуют вход; один компактный блок, безопасное обнуление перед освобождением; RSA хранит CRT-вид, Ed25519 — развёрнутый ключ.
  • Семантика CanSign: четырёхкратная проверка версия + тип + схема + бэкенд; 1.3 отвергает PKCS#1; схема ECDSA следует кривой сертификата; ограниченный PSS-ключ — двусторонние параметрические ограничения.
  • Контракт Sign: двухшаговость (пустой выход запрашивает длину); нехватка ёмкости не зовёт подписчика; неудача не публикует частичную подпись; случайная соль PSS / детерминированный low-S у ECDSA / чистый режим Ed25519.
  • Расширение Create: предикат Supports без ошибки; Sign конкурентен, при неудаче выход неизменен; Release ровно один раз; цепочка сертификатов всё равно глубоко копируется.
  • Модель ошибок: коды ошибок identity в xrt.tls; провалы DER/X.509/crypto сохраняются как Cause — верхний слой может отображать по фазам TLS, C-вызывающий читает нижнюю причину.
  • Обрезка: ядро TLS_IDENTITY + _RSA/_EC/_P256/_P384/_ED25519 независимо — сервер только на Ed25519 не втягивает код RSA.

Типичные ошибки

Ловушка 1: сертификат и ключ не совпали — выяснилось вечером релиза:Ловушка 1: сертификат и ключ не совпали — выяснилось вечером релиза

Симптом: сервис стартует нормально, клиентские соединения падают на этапе CertificateVerify — в журнале лишь «signature invalid», не видно, что перепутан материал.

Причина: многие пути загрузки впервые пользуются приватным ключом только в рукопожатии; перепутанный сертификат (скажем, после продления сменили сертификат, а ключ остался старым) вскрылся первым же реальным соединением.

错误写法
/* только читать файлы, не строить идентичность: ошибка отложена до рукопожатия */
load_file("cert.pem", &Cert, &CertSize);
load_file("key.pem", &Key, &KeySize);
/* сохранить, при рукопожатии использовать напрямую - несовпадение никто не заметит */
正确写法
/* строить идентичность при старте: несовпадение падает на месте */
xtlsidentity* pIdentity = xrtTlsIdentityP256(
	&(xbytesview){ Cert, CertSize }, 1u,
	(xbytesview){ Key, KeySize }
);
if ( pIdentity == NULL ) {
	fprintf(stderr, "identity: %s\n",
		xrtErrorMessage(xrtGetError()));
	return EXIT_FAILURE;   /* вскрытие на этапе старта, с цепочкой причин */
}
Ловушка 2: самодельные комбинации подписных схем (CanSign не прошёл — упорствуем):Ловушка 2: самодельные комбинации подписных схем (CanSign не прошёл — упорствуем)

Симптом: идентичности Ed25519 назначили схему RSA-PSS или P-256-сертификату — схему P-384: IdentitySign возвращает неудачу, рукопожатие висит.

Причина: схемы TLS жёстко связаны с типом ключа (RFC): у схем ECDSA есть имя кривой, RSA делится на семейства rsae/pss, у Ed25519 только чистая схема. Это не «попробовать посмотреть» пункт конфигурации.

错误写法
/* идентичность Ed25519 + схема RSA: неизбежная неудача */
xrtTlsIdentitySign(pIdentity, XTLS_VERSION_13,
	XTLS_SIGNATURE_RSA_PSS_RSAE_SHA256, ...);
正确写法
/* схема следует типу идентичности: спросить CanSign заранее */
xtlssignature Scheme = pick_scheme_for(xrtTlsIdentityType(pIdentity));
if ( !xrtTlsIdentityCanSign(pIdentity, XTLS_VERSION_13, Scheme) ) {
	/* ошибка конфигурации: доложить на старте, не в рукопожатии */
}
Ловушка 3: разделяемое изменяемое состояние в Sign-колбэке HSM-расширения:Ловушка 3: разделяемое изменяемое состояние в Sign-колбэке HSM-расширения

Симптом: при конкурентных рукопожатиях перемежающиеся неудачи подписи либо перепутанный выход — документация Create прямым текстом требует конкурентоспособности Supports/Sign, но реализация колбэка воспользовалась нереентерабельным дескриптором.

Причина: объект идентичности конкурентно разделяется многими Worker-ами; подписные колбэки одновременно бегут в нескольких рукопожатиях. Колбэк — контракт без блокировок; выстроить очередь или погресионные сессии для доступа к устройству с состоянием — ваша ответственность.

错误写法
static bool hsmSign(..., void* pOutput, size_t* pSize) {
	hsm_begin(pHsmSession);   /* разделяемая сессия: конкурентные рукопожатия топчут друг друга */
	hsm_op(...);
	hsm_end(pHsmSession);
}
正确写法
/* на каждый вызов своя сессия (или пул сессий); неудача не меняет выход и длину */
static bool hsmSign(const void* pContent, size_t iSize,
		void* pOutput, size_t iCapacity, size_t* pSize) {
	hsmsession* pS = hsm_session_acquire();   /* через пул */
	bool bOk = hsm_sign(pS, pContent, iSize, pOutput, iCapacity, pSize);
	hsm_session_release(pS);
	return bOk;
}

Практические задания

Базовое: самопроверка четырёх форм на встроенном материале

Прогоните без параметров образец identity и сверьте значения перечисления в четырёх строках с xtlsidentitytype. Затем сгенерируйте OpenSSL самоподписанную пару Ed25519 (сертификат и ключ, экспорт DER) и пройдите параметрический путь с проверкой 64-байтовой подписи. Критерии приёмки: самопроверка четырёх форм вся зелёная; путь на собственном материале выводит identity=5 certificates=1 signature=64 bytes.

Продвинутое: перехват несовпадающего материала на старте

Намеренно перепутайте две пары (сертификат RSA + ключ EC; сертификат P-256 + ключ P-384), постройте каждую идентичность и напечатайте цепочку причин ошибки (xrtErrorMessage + xrtErrorCause). Критерии приёмки: обе пары валятся на конструировании, не на подписи; цепочка ошибок различает «провал DER-разбора» и «несовпадение SPKI».

Вызов: имитатор подписи HSM

Реализуйте через xrtTlsIdentityCreate «имитируемый HSM»: Supports признаёт только Ed25519; Sign внутри — настоящая реализация (семя хранится в «аппаратной» структуре, имитируя неэкспортируемость) плюс атомарный счётчик вызовов. Подключите его к серверу главы 86 (или протестируйте единично), конкурентно подписывая из 8 потоков. Критерии приёмки: все подписи успешны и проверяемы; счётчик равен числу подписей (доказательство конкурентности); «аппаратное» семя ни разу не покидает имитируемую структуру (подтверждение код-ревью).

Итоги

ТемаКратко
Объект идентичностиупаковка цепочки + ключа; неизменяем, подсчёт ссылок, многопоточное разделение без блокировок
Четыре конструктораRsa/P256/P384/Ed25519; диалекты ключей различны (PKCS#1/#8/SEC1/семя)
Проверка на созданииразбор ключа + перекрёстная сверка SPKI + проверка общих схем; нет схем — отказ
Обработка ключаглубокая копия без заимствования; один блок с обнулением перед освобождением; CRT/развёрнутый ключ остаются
CanSignверсия + тип + схема + бэкенд; 1.3 отвергает PKCS#1; ECDSA следует кривой; PSS — двусторонние ограничения
Signдвухшаговый запрос/подпись; неудача не публикует частичное; случайная соль PSS / low-S у ECDSA / чистый Ed25519
Привязка схемсхема следует типу ключа; белый список политики ∩ CanSign — схема рукопожатия
Расширение CreateHSM/системное хранилище/удалённый подписчик; предикат Supports + конкурентный Sign + ровно один Release
Входная границаPEM/файл/системное хранилище вне ядра идентичности — сперва DER, потом конструкция
Обрезка_RSA/_EC/_P256/_P384/_ED25519 независимо; одноалгоритмный сервер не несёт остальных

📖 Справочники API в этой главе:tls (201 записей)tls_identity (18 записей)crypto (140 записей)

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