Skip to content

Commit

Permalink
feat(cryptopro-cades): Добавлена функция генерации контейнера и запро…
Browse files Browse the repository at this point in the history
…са на сертификат (#43)

* feat(cryptopro-cades): Добавлено разворачивание полей доступных считывателей

* feat(cryptopro-cades): Добавлена функция генерации контейнера и запроса на сертификат

* feat(cryptopro-cades): Вынес и описал тип для генерации контейнера и CSR

* doc(cryptopro-cades): Задокументировал функцию создания запроса на сертификат

* refactor(cryptopro-cades): Переименовал и экспортировал функцию запроса на сертификат

* doc(cryptopro-cades): Убрал неверный комментарий к полю

* test(cryptopro-cades): Написал тест на внешнюю функцию

* doc(cryptopro-cades): Задокументировал некоторые константы
  • Loading branch information
aiekseu authored Mar 7, 2024
1 parent a9c779e commit f9fa982
Show file tree
Hide file tree
Showing 15 changed files with 508 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/cryptopro-cades/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ npm install @astral/cryptopro-cades
- getSystemInfo - получение информации о системе пользователя - версия КриптоПро ЭЦП Browser plug-in, версия CSP (VipNet или CryptoPro)
- pluginConfig - возможность включить вывод отладочной информации, подписываться на все создаваемые исключения, отключать проверку корректности системы, ограничивать тип криптопровайдера которым можно пользоваться, его версии.
- getReaders - получение списка доступных считывателей (в т.ч. вставленных токенов) с помощью CryptoPro CSP.
- createCSR - формирование контейнера и запроса на сертификат за один криптосеанс. Работает с VipNet и CryptoPro
196 changes: 196 additions & 0 deletions packages/cryptopro-cades/src/api/createCSR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import {
ALLOW_EXPORT_FLAG,
AT_KEYEXCHANGE,
CERT_POLICY_QUALIFIER_TYPE,
CRYPTO_OBJECTS,
CSP_NAME_MAX_LENGTH,
SUBJECT_SIGN_TOOL_OID,
XCN_CRYPT_STRING_BASE64,
XCN_CRYPT_STRING_BASE64REQUESTHEADER,
} from '../constants';
import { CreateCSRInputDTO } from '../types/СreateCSRInputDTO';
import { outputDebug } from '../utils';

import { createObject } from './createObject';
import { afterPluginLoaded } from './internal/afterPluginLoaded';
import { convertStringToUTF8ByteArray } from './internal/convertStringToUTF8ByteArray';
import { setCryptoProperty } from './internal/setCryptoProperty';

/**
* Функция, формирующая контейнер и запрос на сертификат за один криптосеанс
* @param {CreateCSRInputDTO} data данные для формирования контейнера и запроса на сертификат
* @returns {Promise<string>} Строка, содержащая CSR в DER формате
*/
export const createCSR = (data: CreateCSRInputDTO): Promise<string> => {
return afterPluginLoaded(async () => {
const logData = [];

logData.push({ data });

try {
// Формируем приватный ключ
const pKey = await createObject(CRYPTO_OBJECTS.privateKey);

await setCryptoProperty(pKey, 'ProviderName', data.providerName);
await setCryptoProperty(pKey, 'ProviderType', data.providerCode);
await setCryptoProperty(pKey, 'ContainerName', data.containerName);

await setCryptoProperty(
pKey,
'ExportPolicy',
data.isExportable ? ALLOW_EXPORT_FLAG : 0,
);

// помечаем, что ключ и для шифрования, и для подписывания
// @see https://learn.microsoft.com/ru-ru/windows/win32/api/certenroll/ne-certenroll-x509keyspec
await setCryptoProperty(pKey, 'KeySpec', AT_KEYEXCHANGE);

// инициализируем запрос на сертификат
const certRequestPkcs10 = await createObject(
CRYPTO_OBJECTS.certificateRequest,
);

await certRequestPkcs10.InitializeFromPrivateKey(0x1, pKey, '');

// описываем аттрибуты Subject сертификата
const distinguishedName = await createObject(
CRYPTO_OBJECTS.distinguishedName,
);
const dnValue = data.attributes
.map((attr) => `${attr.oid}="${attr.value}"`)
.join(', ');

await distinguishedName.Encode(dnValue);
await setCryptoProperty(certRequestPkcs10, 'Subject', distinguishedName);

// объект с расширениями, который будем наполнять
const extensions = await certRequestPkcs10.X509Extensions;

// key usages сертификата
const keyUsageExt = await createObject(CRYPTO_OBJECTS.extensionKeyUsage);

await keyUsageExt.InitializeEncode(data.keyUsage);
await extensions.Add(keyUsageExt);

// описываем enhanced key usages сертификата
const enhKeyUsageCollection = await createObject(
CRYPTO_OBJECTS.objectIds,
);

for (const enhKeyOid of data.enhKeyUsage) {
const enhKeyUsageOid = await createObject(CRYPTO_OBJECTS.objectId);

await enhKeyUsageOid.InitializeFromValue(enhKeyOid);
await enhKeyUsageCollection.Add(enhKeyUsageOid);
}

const enhancedKeyUsageExt = await createObject(
CRYPTO_OBJECTS.extensionEnhancedKeyUsage,
);

await enhancedKeyUsageExt.InitializeEncode(enhKeyUsageCollection);
// добавляем enhanced key usages в расширения сертификата
await extensions.Add(enhancedKeyUsageExt);

// описываем политики сертификата
const certPolicyCollection = await createObject(
CRYPTO_OBJECTS.certificatePolicies,
);

for (const policy of data.certPolicies) {
const policyOid = await createObject(CRYPTO_OBJECTS.objectId);

await policyOid.InitializeFromValue(policy.oid);

const certPolicy = await createObject(CRYPTO_OBJECTS.certificatePolicy);

await certPolicy.Initialize(policyOid);

// если oid не полностью определяет политику, то необходимо определить и добавить квалификатор
if (Boolean(policy.value.length)) {
const policyQualifier = await createObject(
CRYPTO_OBJECTS.policyQualifier,
);

await policyQualifier.InitializeEncode(
policy.value,
CERT_POLICY_QUALIFIER_TYPE.UNKNOWN,
);

const policyQualifierCollection = await certPolicy.PolicyQualifiers;

await policyQualifierCollection.Add(policyQualifier);
}

await certPolicyCollection.Add(certPolicy);
}

const certPoliciesExt = await createObject(
CRYPTO_OBJECTS.extensionCertificatePolicies,
);

await certPoliciesExt.InitializeEncode(certPolicyCollection);
// добавляем политики в расширения сертификата
await extensions.Add(certPoliciesExt);

// subject sign tool (custom)
// Необходимо добавлять руками, т.к. плагин может вписать только CryptoPRO CSP 5.0
// @see https://aleksandr.ru/blog/dobavlenie_subjectsigntool_v_kriptopro_ecp_browser_plug_in
// TODO: 09.2024 это поле станет необязательным, можно удалить код после доработок на УЦ
const subjectSignToolOid = await createObject(CRYPTO_OBJECTS.objectId);

await subjectSignToolOid.InitializeFromValue(SUBJECT_SIGN_TOOL_OID);

// необходимо обрезать название (на деле оно всегда < 128 символов)
const shortName = data.signTool.slice(0, CSP_NAME_MAX_LENGTH);
const utf8arr = convertStringToUTF8ByteArray(shortName);

// @see https://learn.microsoft.com/ru-ru/windows/win32/seccertenroll/about-utf8string
const utf8stringTag = 0x0c;

utf8arr.unshift(utf8stringTag, utf8arr.length);

const base64String = btoa(
// @ts-ignore TODO: обновить версию TS, 4.8.3 -> 4.9.5
String.fromCharCode.apply(null, new Uint8Array(utf8arr)),
);

const subjectSignToolExt = await createObject(CRYPTO_OBJECTS.extension);

await subjectSignToolExt.Initialize(
subjectSignToolOid,
XCN_CRYPT_STRING_BASE64,
base64String,
);

// добавляем способ подписания в расширения сертификата
await extensions.Add(subjectSignToolExt);

// identification kind
const identificationKindExt = await createObject(
CRYPTO_OBJECTS.extensionIdentificationKind,
);

await identificationKindExt.InitializeEncode(data.identificationKind);
await extensions.Add(identificationKindExt);

// запрос
const enroll = await createObject(CRYPTO_OBJECTS.enrollment);

await enroll.InitializeFromRequest(certRequestPkcs10);

const csr = await enroll.CreateRequest(
XCN_CRYPT_STRING_BASE64REQUESTHEADER,
);

logData.push({ csr });

return csr;
} catch (error) {
logData.push({ error });
throw error.message;
} finally {
outputDebug('createCSR >>', logData);
}
})();
};
17 changes: 11 additions & 6 deletions packages/cryptopro-cades/src/api/getReaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { CRYPTO_OBJECTS, DEFAULT_CRYPTO_PROVIDER } from '../constants';
import { CryptoError } from '../errors';
import {
type CCspInformation,
type CReaderMode,
type CReaderModes,
type IReaderMode,
} from '../types';
import { outputDebug } from '../utils';

Expand All @@ -15,16 +15,16 @@ import { unwrap } from './internal/unwrap';
/**
* Кэш из доступных считывателей.
*/
let readersCache: CReaderMode[] | null;
let readersCache: IReaderMode[] | null;

/**
* Получить список доступных считывателей (в т.ч. вставленных токенов) с помощью CryptoPro CSP.
* @throws {CryptoError} в случае ошибки.
* @returns {Promise<CReaderMode[]>} Информация о доступных считывателях.
* @returns {Promise<IReaderMode[]>} Информация о доступных считывателях.
*/
export function getReaders(
resetCache: boolean = false,
): Promise<CReaderMode[]> {
): Promise<IReaderMode[]> {
if (readersCache && !resetCache) {
return Promise.resolve(readersCache);
}
Expand All @@ -46,7 +46,7 @@ export function getReaders(
}

const logData = [];
const readers: CReaderMode[] = [];
const readers: IReaderMode[] = [];

try {
const cspInformation: CCspInformation = await createObject(
Expand All @@ -65,7 +65,12 @@ export function getReaders(
for (let i = 0; i < readersCount; i++) {
const reader = await unwrap(readerModes.ItemByIndex(i));

readers.push(reader);
readers.push({
Name: await unwrap(reader.Name),
NickName: await unwrap(reader.NickName),
CarrierFlags: await unwrap(reader.CarrierFlags),
Media: await unwrap(reader.Media),
});
}

return (readersCache = readers);
Expand Down
2 changes: 2 additions & 0 deletions packages/cryptopro-cades/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ export { findCertificateBySkid } from './findCertificateBySkid';
export { checkPlugin } from './checkPlugin';

export { getReaders } from './getReaders';

export { createCSR } from './createCSR';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { convertStringToUTF8ByteArray } from './convertStringToUTF8ByteArray';

describe('convertStringToUTF8ByteArray', () => {
it('Конвертирует название СКЗИ Крипто Про как Тулбокс', () => {
const stringToConvert = '"КриптоПро CSP" (версия 4.0)';
const resultFromToolbox = [
34, 208, 154, 209, 128, 208, 184, 208, 191, 209, 130, 208, 190, 208, 159,
209, 128, 208, 190, 32, 67, 83, 80, 34, 32, 40, 208, 178, 208, 181, 209,
128, 209, 129, 208, 184, 209, 143, 32, 52, 46, 48, 41,
];

const result = convertStringToUTF8ByteArray(stringToConvert);

expect(result).toEqual(resultFromToolbox);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Forked from github.com/google/closure-library
* Converts a JS string to a UTF-8 "byte" array.
* @param {string} str 16-bit unicode string.
* @return {!Array<number>} UTF-8 byte array.
* @see https://github.com/google/closure-library/blob/e877b1eac410c0d842bcda118689759512e0e26f/closure/goog/crypt/crypt.js
*/
export function convertStringToUTF8ByteArray(str: string): number[] {
// TODO(user): Use native implementations if/when available
var out = [],
p = 0;

for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);

if (c < 128) {
out[p++] = c;
} else if (c < 2048) {
out[p++] = (c >> 6) | 192;
out[p++] = (c & 63) | 128;
} else if (
(c & 0xfc00) == 0xd800 &&
i + 1 < str.length &&
(str.charCodeAt(i + 1) & 0xfc00) == 0xdc00
) {
// Surrogate Pair
c = 0x10000 + ((c & 0x03ff) << 10) + (str.charCodeAt(++i) & 0x03ff);
out[p++] = (c >> 18) | 240;
out[p++] = ((c >> 12) & 63) | 128;
out[p++] = ((c >> 6) & 63) | 128;
out[p++] = (c & 63) | 128;
} else {
out[p++] = (c >> 12) | 224;
out[p++] = ((c >> 6) & 63) | 128;
out[p++] = (c & 63) | 128;
}
}

return out;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './convertStringToUTF8ByteArray';
Loading

0 comments on commit f9fa982

Please sign in to comment.