- PVSM.RU - https://www.pvsm.ru -
Приближается вторая половина 2018 года и скоро должен наступить «2000-й год» [1] в ИОК на базе российской криптографии. Это связано с тем, что
использование схемы подписи ГОСТ Р 34.10-2001 для формирования подписи после 31 декабря 2018 года не допускается!
Уже сегодня не имеет смысла получать сертификаты с подписью по ГОСТ Р 34.10-2001.
В тоже время очень много сервисов или приложений разработано на базе OpenSSL, который поддерживал работу с ГОСТ Р 34.10-2001.
Но сегодня в стандартной версии openssl отсутствует поддержка как ГОСТ Р 34.11-2012, так и ГОСТ Р 34.10-2012. Более того в версии 1.1 поддержка криптографии ГОСТ исключена [2] из стандартной поставки («The GOST engine was out of date and therefore it has been removed.»).
Все это заставляет искать альтернативные пути для работы с сертификатами, с электронной подписью («сообщениями формата CMS») и другими объектами ИОК на базе новой российской криптографии.
Одним из таких возможных путей является использование библиотеки GCrypt [3]. В этой библиотеки реализована поддержка новых алгоритмов ГОСТ Р 34.11-2012 (алгоритмы хэширования) и ГОСТ Р 34.10-2012 (алгоритмы подписи).
Итак, начинаем с генерации ключевой пары, содержащей как закрытый, так и открытый ключи. На данный момент в российской криптографии присутствует три типа ключей подписи с соответствующими oid-ами
— ГОСТ Р 34.10-2001 с длиной ключа 256 бит, oid 1.2.643.2.2.19 (0x2a, 0x85, 0x03, 0x02, 0x02, 0x13)
— ГОСТ Р 34.10-2012 с длиной ключа 256 бит (далее ГОСТ Р 34.10-12-256), oid 1.2.643.7.1.1.1.1 (0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x01, 0x01)
— ГОСТ Р 34.10-2012 с длиной ключа 512 бит (далее ГОСТ Р 34.10-12-512), oid 1.2.643.7.1.1.1.2 (0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x01, 0x02)
И сразу отметим, что с точки зрения математики, алгоритмов генерации и их реализации, ключи ГОСТ Р 34.10-2001 и ГОСТ Р 34.10-12-256 абсолютно идентичны! Так же как идентичны алгоритмы формирования электронной подписи на их основе. О каком из этих ключей идет речь можно судить только по информации о ключе, содержащейся, например, в сертификате. Так зачем потребовалось два разных oid-а? Только подчеркнуть тот факт, что при формировании электронной подписи с ключом ГОСТ Р 34.10-2001, должен использоваться хэш, полученный по ГОСТ Р 34.10-94, а при использовании ключа ГОСТ Р 34.10-12-256 должен использоваться хэш, получаемый по ГОСТ Р 34.10-212 с длиной 256 бит. Хотя, чтобы подчеркнуть это, есть оказывается соответствующие oid-ы:
— 1.2.643.2.2.3 (0x2a, 0x85, 0x03, 0x02, 0x02, 0x03) — алгоритм подписи ГОСТ Р 34.10-2001 с ключом 256 с хэшированием ГОСТ Р 34.11-94;
— 1.2.643.7.1.1.3.2 (0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x03, 0x02) — алгоритм подписи ГОСТ Р 34.10-2012 с ключом 256 с хэшированием ГОСТ Р 34.11-2012;
— 1.2.643.7.1.1.3.3 (0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x03, 0x03) — алгоритм подписи ГОСТ Р 34.10-2012 с ключом 512 с хэшированием ГОСТ Р 34.11-2012.
Таким образом, имеется некоторый перебор с oid-ами, но что есть, то есть.
Ключи семейства ГОСТ относятся к семейству ключей на эллиптических кривых. Для генерации ключевой пары необходимо указать базовую точку на эллиптической кривой.
Техническим комитетом по стандартизации «Криптографическая защита информации» (ТК 26 [4]) рекомендованы для использования две базовые точки для ключей ГОСТ Р 34.10-2012-512:
— GOST2012-tc26-A (nickname в терминологии libgcrypt) с oid-ом 1.2.643.7.1.2.1.2.1 (0x2a, 0x85, 0x03, 0x07, 0x01, 0x02, 0x01, 0x02, 0x01);
— GOST2012-tc26-B с oid-ом 1.2.643.7.1.2.1.2.2 (0x2a, 0x85, 0x03, 0x07, 0x01, 0x02, 0x01, 0x02, 0x02);
Для ключей ГОСТ Р 34.10 с длиной 256 бит рекомендованы три базовые точки:
— GOST2001-CryptoPro-A с oid-ом 1.2.643.2.2.35.1 (0x2a, 0x85, 0x03, 0x02, 0x02, 0x23, 0x01);
— GOST2001-CryptoPro-B с oid-ом 1.2.643.2.2.35.2 (0x2a, 0x85, 0x03, 0x02, 0x02, 0x23, 0x02);
— GOST2001-CryptoPro-C с oid-ом 1.2.643.2.2.35.3 (0x2a, 0x85, 0x03, 0x02, 0x02, 0x23, 0x03).
Для ключей ГОСТ Р 34.10 с длиной 256 бит определены еще два oid-а для базовых точек:
— GOST2001-CryptoPro-XchA с oid-ом 1.2.643.2.2.36.0 (0x2a, 0x85, 0x03, 0x02, 0x02, 0x24, 0x00);
— GOST2001-CryptoPro-XchB с oid-ом 1.2.643.2.2.36.1 (0x2a, 0x85, 0x03, 0x02, 0x02, 0x24, 0x01).
Однако в реальности эти oid-ы ссылаются на бозовые точки GOST2001-CryptoPro-A с oid-ом 1.2.643.2.2.35.1 и GOST2001-CryptoPro-C с oid-ом 1.2.643.2.2.35.3 соответственно.
И это следует учитывать при обработке базовых точек с этими oid-ами.
Для генерации ключевой пары используется функция gcry_pk_genkey следующего вида:
gcry_error_t gcry_pk_genkey (gcry sexp t *key_pair, gcry sexp t key_spec ).
В переменной parms должны быть установлены параметры для генерации ключевой пары в формате внутреннего S-выражения [5] (sexp). Для генерации ключевой пары по ГОСТ параметры задаются в виде следующего S-выражения:
(genkey (есс (curve базовая_точка))), где
ecc определяет генерацию ключевой пары на эллиптических кривых, а базовая точка должна указывать конкретную точку, рекомендуемую ТК-26. Именно указанная базовая точка определяет какая ключевая пара будет сгенерирована. Если указать, например, GOST2012-tc26-A или GOST2012-tc26-В, то будет сгенерирована ключевая пара по ГОСТ Р 34.10-2012 с длиной ключа 512 бит. Вместо nickname базовой точки можно указывать напрямую oid:
(genkey (есс (curve «1.2.643.2.2.35.3»)))
В последнем случае предполагается генерация ключевой пары по ГОСТ Р 34.10 с длиной ключа 256 бит и базовой точкой GOST2001-CryptoPro-C.
Ниже приведен пример программы на языке C, который демонстрирует генерацию ключей,
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <gcrypt.h>
/*Функция печати S-выражений*/
static void
show_sexp (const char *prefix, gcry_sexp_t a)
{
char *buf;
size_t size;
if (prefix)
fputs (prefix, stderr);
size = gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, NULL, 0);
buf = gcry_xmalloc (size);
gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, buf, size);
fprintf (stderr, "%.*s", (int)size, buf);
gcry_free (buf);
}
int main(int argc, char* argv[]) {
gpg_error_t err;
gcry_sexp_t key_spec, key_pair, pub_key, sec_key;
/*Базовые точки*/
char *curve_gost[] = {"GOST2001-CryptoPro-A", "GOST2001-CryptoPro-B", "GOST2001-CryptoPro-C", "GOST2012-tc26-A", "GOST2012-tc26-B", NULL};
/*Задаем параметры для генерации ключевой пары*/
err = gcry_sexp_build (&key_spec, NULL,
"(genkey (ecc (curve %s)))", curve_gost[1]);
if (err) {
fprintf(stderr, "creating S-expression failed: %sn", gcry_strerror (err));
exit (1);
}
err = gcry_pk_genkey (&key_pair, key_spec);
if (err){
fprintf(stderr, "creating %s key failed: %sn", argv[1], gcry_strerror (err));
exit(1);
}
/*Печать S-выражения ключевой пары*/
show_sexp ("ECC GOST key pair:n", key_pair);
/*Сохраняем публичный ключ*/
pub_key = gcry_sexp_find_token (key_pair, "public-key", 0);
if (! pub_key) {
fprintf(stderr, "public part missing in keyn");
exit(1);
}
/*Печать S-выражения публичного ключа*/
show_sexp ("ECC GOST public key:n", pub_key);
/*Сохраняем закрытый ключ*/
sec_key = gcry_sexp_find_token (key_pair, "private-key", 0);
if (! sec_key){
fprintf(stderr, "private part missing in keyn");
exit(1);
}
/*Печать S-выражения закрытого ключа*/
show_sexp ("ECC GOST private key:n", sec_key);
/*Освобождаем память, занимаемую ключевой парой*/
gcry_sexp_release (key_pair);
/*Освобождаем память, занимаемую параметрами ключевой пары*/
gcry_sexp_release (key_spec);
}
Для трансляции примера необходимо выпонить команду:
$cc –o GenKey GenKey.c –lgcrypt
$
После запуска модуля GenKey получим
ECC GOST key pair:
(key-data
(public-key
(ecc
(curve GOST2001-CryptoPro-B)
(q #043484CF83F837AAC7ABD4707DE27F5A1F6161120C0D77B63DFFC7D50A7772A12D1E836E6257766E8B83209DD59845F8080BA29E9A86D0A6B6C2D68F44650B3A14#)
)
)
(private-key
(ecc
(curve GOST2001-CryptoPro-B)
(q #043484CF83F837AAC7ABD4707DE27F5A1F6161120C0D77B63DFFC7D50A7772A12D1E836E6257766E8B83209DD59845F8080BA29E9A86D0A6B6C2D68F44650B3A14#)
(d #1ABB5A62BFF88C97567B467C6F4017242FE344B4F4BC8906CE40A0F9D51CBE48#)
)
)
)
ECC GOST public key:
(public-key
(ecc
(curve GOST2001-CryptoPro-B)
(q #043484CF83F837AAC7ABD4707DE27F5A1F6161120C0D77B63DFFC7D50A7772A12D1E836E6257766E8B83209DD59845F8080BA29E9A86D0A6B6C2D68F44650B3A14#)
)
)
ECC GOST private key:
(private-key
(ecc
(curve GOST2001-CryptoPro-B)
(q #043484CF83F837AAC7ABD4707DE27F5A1F6161120C0D77B63DFFC7D50A7772A12D1E836E6257766E8B83209DD59845F8080BA29E9A86D0A6B6C2D68F44650B3A14#)
(d #1ABB5A62BFF88C97567B467C6F4017242FE344B4F4BC8906CE40A0F9D51CBE48#)
)
)
Теперь, имея на руках закрытый ключ, можно создавать электронную подпись.
Создание электронной подписи (ЭП) документа начинается с получения значения хэша от подписываемого документа. Для подписи документа может быть выбран один из алгоритмов:
— 1.2.643.2.2.3 (0x2a, 0x85, 0x03, 0x02, 0x02, 0x03) — алгоритм подписи ГОСТ Р 34.10-2001 с ключом 256 с хэшированием по ГОСТ Р 34.11-94 с длиной хэша 256 бит;
— 1.2.643.7.1.1.3.2 (0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x03, 0x02) — алгоритм подписи ГОСТ Р 34.10-2012 с ключом 256 с хэшированием по ГОСТ Р 34.11-2012 с длиной хэша 256 бит;
— 1.2.643.7.1.1.3.3 (0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x03, 0x03) — алгоритм подписи ГОСТ Р 34.10-2012 с ключом 512 с хэшированием по ГОСТ Р 34.11-2012 с длиной хэша 512 бит.
Эти алгоритмы определяют не только тип закрытого ключа, который будет использован для получения ЭП, но и алгоритм хэш-функции. В библиотеке GCrypt реализованы все три типа функций. Алгоритм хэширования ГОСТ Р 34.11-94 (с oid-ом параметра 1.2.643.2.2.30.1 — 0x2a, 0x85, 0x03, 0x02, 0x02, 0x1e, 0x01) реализован под nickname-ом GOSTR3411_CP, алгоритм ГОСТ Р 34.11-2012 с длиной 256 бит (oid 1.2.43.7.1.1.2.2 — 0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x02, 0x02) реализован под nickname-ом STRIBOG256 и алгоритм ГОСТ Р 34.11-2012 с длиной 512 бит (oid 1.2.43.7.1.1.2.3 — 0x2a, 0x85, 0x03, 0x07, 0x01, 0x01, 0x02, 0x03) реализован под nickname-ом STRIBOG512.
Ниже приведен код модуля на языке C, для вычисления значения хэш от документа, хранящегося в файле
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <gcrypt.h>
int main(int argc, char* argv[]) {
gpg_error_t err;
int algo = GCRY_MD_NONE;
int i;
unsigned char *h;
size_t size;
gcry_md_hd_t hd;
FILE *fp;
unsigned char buf[1024];
char *dgst_gost[] = {"GOSTR3411_CP", "STRIBOG256", "STRIBOG512", NULL};
i = 0;
/*Проверка параметров хэш-функции*/
if (argc == 3)
algo = gcry_md_map_name (argv[1]);
if (algo == GCRY_MD_NONE) {
fprintf(stderr, "Usage: digest_gcrypt <nick_name_digest> <file_for_digest>n");
fprintf(stderr, "<nick_name_digest>=");
while (dgst_gost[i] != NULL){
if (i > 0 )
fprintf(stderr, " | ", dgst_gost[i]);
fprintf(stderr, "%s", dgst_gost[i]);
i++;
}
fprintf(stderr, "n");
exit (1);
}
/*Создаем контекст хэш функции*/
err = gcry_md_open(&hd, algo, 0);
if (err) {
fprintf (stderr, "LibGCrypt error %sn",
gcry_strerror (err));
exit (1);
}
/*Открываем подписываемый файл*/
if (!strcmp (argv[2], "-"))
fp = stdin;
else
fp = fopen (argv[2], "r");
if (fp == NULL) {
fprintf(stderr, "Cannot fopen file=%sn", argv[2]);
exit(1);
}
/*Считаем хэш содержимого файла*/
while (!feof (fp)) {
size = fread (buf, 1, sizeof(buf), fp);
gcry_md_write (hd, buf, size);
}
/*Сохраняем значение хэш*/
h = gcry_md_read(hd, 0);
/*Печатаем значение хэш*/
printf("Длина хеш %s = %d (в байтах)n", argv[1], gcry_md_get_algo_dlen (algo));
printf("Хэш от файла %s:n", argv[2]);
for (i = 0; i < gcry_md_get_algo_dlen (algo); i++)
printf("%02x", h[i]);
printf("n");
fflush(stdout);
/*Закрываем контекст*/
gcry_md_reset(hd);
gcry_md_close(hd);
}
Транслируем этот модуль и получаем утилиту вычисления хэш:
$cc -o digest_file digest_file.c -lgcrypt
$./digest_file
Usage: digest_gcrypt <nick_name_digest> <file_for_digest>
<nick_name_digest>=GOSTR3411_CP | STRIBOG256 | STRIBOG512
$./digest_file STRIBOG256 digest_file.c
Длина хеш STRIBOG256 = 32 (в байтах)
Хэш от файла digest_file.c:
f6818dfb26073747266dc721c332d703eb21f2b17e3433c809e0e23b68443d4a
$
Теперь, когда имеется закрытый ключ и хэш документа, мы можем сформировать и электронную подпись документа:
gcry_error_t gcry_pk_sign (gcry sexp t *r_sig, gcry sexp t data, gcry sexp t skey ), где
r_sig -sexp-переменная, в которой будет сохранена электронная подпись,
skey – sexp-переменная с закрытым ключом (см. выше), который используется для формирования подписи,
data – sexp-переменная, в которой указываются тип подписи (в нашем случае gost) и хэш подписываемого документа.
Здесь следует помнить, что значение хэш, поступающее на вход gcry_pk_sign для формирования подписи ГОСТ Р 34.10 должно быть в формате big-endian, т.е. фактически перевернуто (как сказал один товарищ: «Российская традиция рассматривать байты дайджеста в порядке little-endian»). С учетом этого замечания подготовка sexp-переменной data выглядит следующим образом:
...
gcry_sexp_t data;
unsigned char c;
int len_xy;
gcry_mpi_t x;
…
/*Проверяем архитектуру и инвертируем хэш*/
printf("%sn", *((unsigned char *) &arch) == 0 ? "Архитектура big-endian" : "Архитекткра little-endian");
len_xy = *((unsigned char *) &arch) == 0 ? 0:gcry_md_get_algo_dlen (algo);
for (i = 0; i < (len_xy/2); i++) {
c = *(h + i);
*(h + i) = *(h + len_xy - i - 1);
*(h + len_xy - i - 1) = c;
}
fprintf(stderr, "Хэш длина=%dn", gcry_md_get_algo_dlen (algo));
for (i = 0; i < gcry_md_get_algo_dlen (algo); i++)
printf("%02X", h[i]);
fflush(stdout);
/*Сохраняем хэш в mpi-переменной*/
x = gcry_mpi_set_opaque_copy(NULL, h, gcry_md_get_algo_dlen (algo) * 8);
/*Инициализируем sexp-переменную data исходными данными – тип и хэш*/
err = gcry_sexp_build (&data, NULL, "(data (flags gost) (value %m))", x);
/* Печатаем содержимое переменной data */
show_sexp ("data :n", data);
Все готово для получения подписи и ее просмотра
…
gcry_sexp_t sig_r, sig_s;
…
/*Подписываем хэш*/
err = gcry_pk_sign (&sig, data, sec_key);
if (err) {
fprintf(stderr, "signing failed: %sn", gcry_strerror (err));
exit(1);
}
/* Печатаем подпись */
show_sexp ("ECC GOST SIG:n", sig);
/*Выбираем и печаем составные части подписи r и s*/
sig_r = gcry_sexp_find_token (sig, "r", 0);
if (!sig_r) {
fprintf(stderr, "r part missing in sign");
exit(1);
}
show_sexp ("ECC GOST Sig R-part:n", sig_r);
sig_s = gcry_sexp_find_token (sig, "s", 0);
if (! sig_s) {
fprintf(stderr, "s part missing in sign");
exit(1);
}
show_sexp ("ECC GOST Sig S-part:n", sig_s);
…
Проверить подпись можно следующим образом:
…
err = gcry_pk_verify (sig, data, pub_key);
if (err) {
putchar ('n');
show_sexp ("seckey:n", sec_key);
show_sexp ("data:n", data);
show_sexp ("sig:n", sig);
fprintf(stderr, "verify failed: %sn", gcry_strerror (err));
exit(1);
}
…
[6]Одними из главных объектов инфраструктуры открытых ключей (ИОК) являются сертификаты X509. Рекомендации ТК-26 по составу и структуре сертификатов изложены в документе «ТЕХНИЧЕСКАЯ СПЕЦИФИКАЦИЯ ИСПОЛЬЗОВАНИЯ АЛГОРИТМОВ ГОСТ Р 34.10, ГОСТ Р 34.11 В ПРОФИЛЕ СЕРТИФИКАТА И СПИСКЕ ОТЗЫВА СЕРТИФИКАТОВ (CRL) ИНФРАСТРУКТУРЫ ОТКРЫТЫХ КЛЮЧЕЙ X.509 (Утверждена решением заседания технического комитета по стандартизации «Криптографическая защита информации» (Протокол N13 от 24.04.2014 г.) ». Именно в соответствии с этими рекомендациями выпускают сертификаты все аккредитованные в Минкомсвязи России УЦ [6].
Для проверки подписи в сертификате необходимо (см. выше) получить хэш проверяемого сертификата, его подпись и открытый ключ корневого сертификата. Сразу отметим, что для самоподписанного сертификата все эти данные хранятся в одном сертификате.
Для работы с объектами ИОК/PKI (сертификаты, CMS, запросы [3]
и т.п.), как правило, используется библиотека KSBA, которая на данный момент не поддерживает рекомендации ТК-26, хотя опыт [3] такой поддержки есть. В принципе, ничто не мешает добавить поддержку рекомендаций ТК-26 в проект ksba.
На данном этапе, этапе тестирования GCrypt, для работы с объектами ИОК/PKI (сертификаты и т.п.) с россицйской криптографией удобно использовать скриптовые языки типа Python [7], Tcl и т.д. Был выбран скриптовый язык Tcl [8]. На нем легко и просто написать прототип программы, который затем перенести на язык C. В составе Tcl имеется пакет PKI, который содержит процедуры разбора (parse) объектов ИОК, в частности, процедура разбора сертификатов ::pki::x509::parse_cert. На базе процедуры parse_cert была разработана процедура parse_gost_cert, которая находится в файле
proc parse_cert_gost {cert} {
# parray ::pki::oids
#puts "parse_cert_gost=$cert"
set cert_seq ""
if { [string range $cert 0 9 ] == "-----BEGIN" } {
array set parsed_cert [::pki::_parse_pem $cert "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]
set cert_seq $parsed_cert(data)
} else {
#FORMAT DER
set cert_seq $cert
}
set finger [::sha1::sha1 $cert_seq]
set ret(fingerprint) $finger
binary scan $cert_seq H* certdb
set ret(certdb) $certdb
#puts "CERTDB=$certdb"
array set ret [list]
# Decode X.509 certificate, which is an ASN.1 sequence
::asn::asnGetSequence cert_seq wholething
::asn::asnGetSequence wholething cert
set ret(cert) $cert
set ret(cert) [::asn::asnSequence $ret(cert)]
if {0} {
set ff [open "/tmp/tbs.der" w]
fconfigure $ff -translation binary
puts -nonewline $ff $ret(cert)
close $ff
}
binary scan $ret(cert) H* ret(cert)
::asn::asnPeekByte cert peek_tag
if {$peek_tag != 0x02} {
# Version number is optional, if missing assumed to be value of 0
::asn::asnGetContext cert - asn_version
::asn::asnGetInteger asn_version ret(version)
incr ret(version)
} else {
set ret(version) 1
}
::asn::asnGetBigInteger cert ret(serial_number)
::asn::asnGetSequence cert data_signature_algo_seq
::asn::asnGetObjectIdentifier data_signature_algo_seq ret(data_signature_algo)
::asn::asnGetSequence cert issuer
::asn::asnGetSequence cert validity
::asn::asnGetUTCTime validity ret(notBefore)
::asn::asnGetUTCTime validity ret(notAfter)
::asn::asnGetSequence cert subject
::asn::asnGetSequence cert pubkeyinfo
binary scan $pubkeyinfo H* pubkeyinfoG
set ret(pubkeyinfo) $pubkeyinfoG
::asn::asnGetSequence pubkeyinfo pubkey_algoid
binary scan $pubkey_algoid H* pubkey_algoidG
set ret(pubkey_algoid) $pubkey_algoidG
::asn::asnGetObjectIdentifier pubkey_algoid ret(pubkey_algo)
::asn::asnGetBitString pubkeyinfo pubkey
set extensions_list [list]
while {$cert != ""} {
::asn::asnPeekByte cert peek_tag
switch -- [format {0x%02x} $peek_tag] {
"0xa1" {
::asn::asnGetContext cert - issuerUniqID
}
"0xa2" {
::asn::asnGetContext cert - subjectUniqID
}
"0xa3" {
::asn::asnGetContext cert - extensions_ctx
::asn::asnGetSequence extensions_ctx extensions
while {$extensions != ""} {
::asn::asnGetSequence extensions extension
::asn::asnGetObjectIdentifier extension ext_oid
::asn::asnPeekByte extension peek_tag
if {$peek_tag == 0x1} {
::asn::asnGetBoolean extension ext_critical
} else {
set ext_critical false
}
::asn::asnGetOctetString extension ext_value_seq
set ext_oid [::pki::_oid_number_to_name $ext_oid]
set ext_value [list $ext_critical]
switch -- $ext_oid {
id-ce-basicConstraints {
::asn::asnGetSequence ext_value_seq ext_value_bin
if {$ext_value_bin != ""} {
::asn::asnGetBoolean ext_value_bin allowCA
} else {
set allowCA "false"
}
if {$ext_value_bin != ""} {
::asn::asnGetInteger ext_value_bin caDepth
} else {
set caDepth -1
}
lappend ext_value $allowCA $caDepth
}
default {
binary scan $ext_value_seq H* ext_value_seq_hex
lappend ext_value $ext_value_seq_hex
}
}
lappend extensions_list $ext_oid $ext_value
}
}
}
}
set ret(extensions) $extensions_list
::asn::asnGetSequence wholething signature_algo_seq
::asn::asnGetObjectIdentifier signature_algo_seq ret(signature_algo)
::asn::asnGetBitString wholething ret(signature)
# Convert values from ASN.1 decoder to usable values if needed
set ret(notBefore) [::pki::x509::_utctime_to_native $ret(notBefore)]
set ret(notAfter) [::pki::x509::_utctime_to_native $ret(notAfter)]
set ret(serial_number) [::math::bignum::tostr $ret(serial_number)]
set ret(data_signature_algo) [::pki::_oid_number_to_name $ret(data_signature_algo)]
set ret(signature_algo) [::pki::_oid_number_to_name $ret(signature_algo)]
set ret(pubkey_algo) [::pki::_oid_number_to_name $ret(pubkey_algo)]
set ret(issuer) [::pki::x509::_dn_to_string $issuer]
set ret(subject) [::pki::x509::_dn_to_string $subject]
set ret(signature) [binary format B* $ret(signature)]
binary scan $ret(signature) H* ret(signature)
# Handle RSA public keys by extracting N and E
#puts "PUBKEY_ALGO=$ret(pubkey_algo)"
switch -- $ret(pubkey_algo) {
"rsaEncryption" {
set pubkey [binary format B* $pubkey]
binary scan $pubkey H* ret(pubkey)
::asn::asnGetSequence pubkey pubkey_parts
::asn::asnGetBigInteger pubkey_parts ret(n)
::asn::asnGetBigInteger pubkey_parts ret(e)
set ret(n) [::math::bignum::tostr $ret(n)]
set ret(e) [::math::bignum::tostr $ret(e)]
set ret(l) [expr {int([::pki::_bits $ret(n)] / 8.0000 + 0.5) * 8}]
set ret(type) rsa
}
"GostR2012_256" -
"GostR2012_512" -
"GostR2001" -
"1.2.643.2.2.19" -
"1.2.643.7.1.1.1.1" -
"1.2.643.7.1.1.1.2" {
# gost2001, gost2012-256,gost2012-512
set pubkey [binary format B* $pubkey]
#puts "LL=[string length $pubkey]"
if {[string length $pubkey] < 100} {
set pubk [string range $pubkey 2 end]
} else {
set pubk [string range $pubkey 3 end]
}
set pubkey_revert [string reverse $pubk]
binary scan $pubkey_revert H* ret(pubkey_rev)
binary scan $pubkey H* ret(pubkey)
set ret(type) gost
::asn::asnGetSequence pubkey_algoid pubalgost
#OID - параметра
::asn::asnGetObjectIdentifier pubalgost ret(paramkey)
set ret(paramkey) [::pki::_oid_number_to_name $ret(paramkey)]
#OID - Функция хэша
::asn::asnGetObjectIdentifier pubalgost ret(hashkey)
set ret(hashkey) [::pki::_oid_number_to_name $ret(hashkey)]
#puts "ret(paramkey)=$ret(paramkey)n"
#puts "ret(hashkey)=$ret(hashkey)n"
}
}
return [array get ret]
}
proc set_nick_for_oid {} {
# set ::pki::oids(1.2.643.2.2.19) "gost2001pubKey"
# set ::pki::oids(1.2.643.2.2.3) "gost2001withGOST3411_94"
set ::pki::oids(1.2.643.100.1) "OGRN"
set ::pki::oids(1.2.643.100.5) "OGRNIP"
set ::pki::oids(1.2.643.3.131.1.1) "INN"
set ::pki::oids(1.2.643.100.3) "SNILS"
#Алгоритмы подписи
set ::pki::oids(1.2.643.2.2.3) "ГОСТ Р 34.10-2001-256"
set ::pki::oids(1.2.643.7.1.1.3.2) "ГОСТ Р 34.10-2012-256"
set ::pki::oids(1.2.643.7.1.1.3.3) "ГОСТ Р 34.10-2012-512"
# set ::pki::oids(1.2.643.2.2.3) "gost"
# set ::pki::oids(1.2.643.7.1.1.3.2) "gost"
# set ::pki::oids(1.2.643.7.1.1.3.3) "gost"
#Алгоритмы ключа
# set ::pki::oids(1.2.643.2.2.19) "ГОСТ Р 34.10-2001"
# set ::pki::oids(1.2.643.7.1.1.1.1) "ГОСТ Р 34.10-2012 256 байт"
# set ::pki::oids(1.2.643.7.1.1.1.2) "ГОСТ Р 34.10-2012 512 байт"
set ::pki::oids(1.2.643.2.2.19) "GostR2001"
set ::pki::oids(1.2.643.7.1.1.1.1) "GostR2012_256"
set ::pki::oids(1.2.643.7.1.1.1.2) "GostR2012_512"
#Oid-ы параметров ключа ГОСТ Р 34.10-2001 и ГОСТ Р 34.10-2012-256
set ::pki::oids(1.2.643.2.2.35.0) "GOST2001-test"
set ::pki::oids(1.2.643.2.2.35.1) "GOST2001-CryptoPro-A"
set ::pki::oids(1.2.643.2.2.35.2) "GOST2001-CryptoPro-B"
set ::pki::oids(1.2.643.2.2.35.3) "GOST2001-CryptoPro-C"
# { "GOST2001-CryptoPro-A", set ::pki::oids("GOST2001-CryptoPro-XchA" },
# { "GOST2001-CryptoPro-C", set ::pki::oids("GOST2001-CryptoPro-XchB" },
set ::pki::oids(1.2.643.2.2.36.0) "GOST2001-CryptoPro-A"
set ::pki::oids(1.2.643.2.2.36.1) "GOST2001-CryptoPro-C"
#Oid-ы параметров ключа ГОСТ Р 34.10-2012-512
set ::pki::oids(1.2.643.7.1.2.1.2.1) "GOST2012-tc26-A"
set ::pki::oids(1.2.643.7.1.2.1.2.2) "GOST2012-tc26-B"
#Nick для хэш
set ::pki::oids(1.2.643.7.1.1.2.2) "STRIBOG256"
set ::pki::oids(1.2.643.7.1.1.2.3) "STRIBOG512"
set ::pki::oids(1.2.643.2.2.30.1) "GOSTR3411_CP"
}
Процедура parse_gost_cert предназначена для разбора сертификатов на базе российской криптографии.
В этом файле находится также процедура присвоения nickname-мов oid-ам российской криптографии с учетом проекта GCrypt:
proc set_nick_for_oid {} {
set ::pki::oids(1.2.643.100.1) "OGRN"
set ::pki::oids(1.2.643.100.5) "OGRNIP"
set ::pki::oids(1.2.643.3.131.1.1) "INN"
set ::pki::oids(1.2.643.100.3) "SNILS"
#Алгоритмы подписи
set ::pki::oids(1.2.643.2.2.3) "ГОСТ Р 34.10-2001-256"
set ::pki::oids(1.2.643.7.1.1.3.2) "ГОСТ Р 34.10-2012-256"
set ::pki::oids(1.2.643.7.1.1.3.3) "ГОСТ Р 34.10-2012-512"
#Алгоритмы ключа
set ::pki::oids(1.2.643.2.2.19) "GostR2001"
set ::pki::oids(1.2.643.7.1.1.1.1) "GostR2012_256"
set ::pki::oids(1.2.643.7.1.1.1.2) "GostR2012_512"
#Oid-ы параметров ключа ГОСТ Р 34.10-2001 и ГОСТ Р 34.10-2012-256
set ::pki::oids(1.2.643.2.2.35.0) "GOST2001-test"
set ::pki::oids(1.2.643.2.2.35.1) "GOST2001-CryptoPro-A"
set ::pki::oids(1.2.643.2.2.35.2) "GOST2001-CryptoPro-B"
set ::pki::oids(1.2.643.2.2.35.3) "GOST2001-CryptoPro-C"
set ::pki::oids(1.2.643.2.2.36.0) "GOST2001-CryptoPro-A"
set ::pki::oids(1.2.643.2.2.36.1) "GOST2001-CryptoPro-C"
#Oid-ы параметров ключа ГОСТ Р 34.10-2012-512
set ::pki::oids(1.2.643.7.1.2.1.2.1) "GOST2012-tc26-A"
set ::pki::oids(1.2.643.7.1.2.1.2.2) "GOST2012-tc26-B"
#Nick для хэш
set ::pki::oids(1.2.643.7.1.1.2.2) "STRIBOG256"
set ::pki::oids(1.2.643.7.1.1.2.3) "STRIBOG512"
set ::pki::oids(1.2.643.2.2.30.1) "GOSTR3411_CP"
}
Процедура parse_gost_cert позволяет получить TBS-сертификат [9], подпись сертификата, тип подписи, открытый ключ. Если мы рассматриваем самоподписанный сертификат, то этого достаточно для проверки подписи. Если же мы проверяем подпись сертификата, подписанного (выпущенного) другим сертификатом (issuer и subject сертификата не совпадают), то процедура получения информации для проверки подписи выглядит следующим образом:
— из проверяемого сертификата извлекаем его TBS-сертификат, тип подписи и саму подпись:
— из корневого сертификата извлекаем открытый ключ.
Самое ответственное при подготовке исходных данных для проверки подписи сертификата, это строгое следование рекомендациям TK-26. Для значения открытого ключа они звучат так:
Представление открытого ключа GostR3410-2012-256-PublicKey идентично представлению открытого ключа ГОСТ Р 34.10-2001 [IETF RFC 4491], и ДОЛЖНО содержать 64 октета, где первые 32 октета содержат координату x в представлении little-endian, и вторые 32 октета содержат координату y в представлении little-endian.
Представление открытого ключа GostR3410-2012-512-PublicKey ДОЛЖНО содержать
128 октетов, где первые 64 октета содержат координату x в представлении little-endian, и вторые 64 октета содержат координату у в представлении little-endian
При выгрузке подписи следует руководствоваться следующим:
Алгоритм подписи ГОСТ Р 34.10-2012 с длиной хэш-кода 256 бит используется для формирования цифровой подписи в форме двух 256-битных чисел, r и s. Её представление в виде строки октетов (OCTET STRING) идентично представлению подписи ГОСТ Р 34.10-2001 [IETF RFC 4491] и состоит из 64 октетов; при этом первые 32 октета содержат число s в представлении big-endian (старший октет записывается первым), а вторые 32 октета содержат число r в представлении big-endian.
Алгоритм подписи ГОСТ Р 34.10-2012 с длиной хэш-кода 512 используется для формирования цифровой подписи в форме двух 512-битных чисел, открытые ключи согласно r и s. Её представление в виде строки октетов (OCTET STRING) состоит из 128 октетов; при этом первые 64 октета содержат число s в представлении big-endian (старший октет записывается первым), а вторые 64 октета содержат число r в представлении big-endian.
С учетом этих рекомендаций Tcl-модуль parse_certs_for_verify_load.tcl подготовки исходных данных для проверки подписи сертификата
#!/usr/bin/tclsh
#загружаем пакет PKI
package require pki
#загружаем файл с процедурой разбора ГОСТ-ых сертификатов
source parse_cert_gost_oid.tcl
if {$argc != 2} {
puts "Usage: parse_certs_for_verify_load.tcl <проверяемый сертификат> <корневой сертификат>"
exit 1
}
#Проверяемый сертификат
if {[file exists "[lindex $argv 0]"] == 0 } {
puts "Usage: parse_certs_for_verify_load.tcl <проверяемый сертификат> <корневой сертификат>"
puts "Отсутствует файл [lindex $argv 0]"
exit 1
}
#Корневой сертификат
if {[file exists "[lindex $argv 1]"] == 0 } {
puts "Usage: parse_certs_for_verify_load.tcl <проверяемый сертификат> <корневой сертификат>"
puts "Отсутствует файл [lindex $argv 1]"
exit 1
}
#Устанавливаем nick-и для ГОСТ-овых oid-ов
set_nick_for_oid
set file [lindex $argv 0]
set f [open $file r]
set cert [read $f]
close $f
#READ DER-format
if { [string range $cert 0 9 ] != "-----BEGIN" } {
set fd [open $file]
chan configure $fd -translation binary
set cert [read $fd]
close $fd
}
array set cert_user [parse_cert_gost $cert]
#Читай рекомендации ТК-26
set len_sign [expr [string length $cert_user(signature)] /2]
set sign_r [string range $cert_user(signature) $len_sign end]
set sign_s [string range $cert_user(signature) 0 [expr $len_sign - 1]]
#puts "Корневой сертификат: $file"
set file [lindex $argv 1]
set f [open $file r]
set cert [read $f]
close $f
#READ DER
if { [string range $cert 0 9 ] != "-----BEGIN" } {
set fd [open $file]
chan configure $fd -translation binary
set cert [read $fd]
close $fd
}
#Распарсиваем в массив корневой сертификат
array set cert_ca [parse_cert_gost $cert]
#Читай рекомендации ТК-26
set len_key [expr [string length $cert_ca(pubkey_rev)]/2]
set key_pub_left [string range $cert_ca(pubkey_rev) $len_key end]
set key_pub_right [string range $cert_ca(pubkey_rev) 0 [expr $len_key - 1]]
puts "/*Делаем C-код: Исходные данные для проверки подписи сертификата*/"
#TBS-проверяемого сертификата
puts "char tbc[] = "[string toupper $cert_user(cert)]";"
#Тип хэш-функции
puts "char hash_type[] = "$cert_ca(hashkey)";"
#Открытый ключ корневого сертификата
puts "unsigned char pub_key_ca[] = "(public-key ""
puts ""(ecc ""
puts "" (curve $cert_ca(paramkey))""
puts "" (q #04[string toupper $key_pub_left$key_pub_right]#)""
puts "")""
puts "")";"
#Подпись проверяемого сертификата
puts "unsigned char sig_cert[] = "(sig-val""
puts ""($cert_ca(type) ""
puts "" (r #[string toupper $sign_r]#)""
puts "" (s #[string toupper $sign_s]#)""
puts "")""
puts "")";"
puts "/*Сохраните вывод в файле TEST_from_TCL.h*/"
puts "/*Оттранслируйте модуль TEST_from_Tcl.c: cc -o TEST_from_Tcl TEST_from_Tcl.c -lgcrypt и выполните модуль TEST_from_Tcl*/"
Для тестирования возьмем два реальных сертификата:
Подготовим исходные данные для проверки сертификата «УЦ 1 ИС ГУЦ.pem»:
$ ./parse_certs_for_verify_load.tcl "УЦ 1 ИС ГУЦ.pem" "ГУЦ Минкомсвязь.pem" > TEST_from_TCL.h
$echo "Содержимое файла TEST_from_TCL.h"
$cat TEST_from_TCL.h
/*Делаем C-код: Исходные данные для проверки подписи сертификата*/
char tbc[] = "3082065B . . . ";
char hash_type[] = "GOSTR3411_CP";
unsigned char pub_key_ca[] = "(public-key "
"(ecc "
" (curve GOST2001-CryptoPro-A)"
" (q #040FA8E8C365FBA9792AF7293FF4838AC59BBE5B573164AD91D6C8231079BFA58FFBA1377303A17E90E62EDC8462730D8BD5F93A70DA8A213FC85F915422C4755A#)"
")"
")";
unsigned char sig_cert[] = "(sig-val"
"(gost "
" (r #913931E75D87DD82EA58CACE2E321F54C26173E0D308D797598E9B84AE3F0D15#)"
" (s #A558A2452F6FB5C030F245DFC64F28ED8114195D98611FAAC7E6B223B7E58E76#)"
")"
")";
/*Сохраните вывод в файле TEST_from_TCL.h*/
/*Оттранслируйте модуль TEST_from_Tcl.c: cc -o TEST_from_Tcl TEST_from_Tcl.c -lgcrypt и выполните модуль TEST_from_Tcl*/
$
Проверка подписи сертификата выполняется модулем:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <gcrypt.h>
#include "TEST_from_TCL.h"
#define digitp(p) (*(p) >= '0' && *(p) <= '9')
#define hexdigitp(a) (digitp (a)
|| (*(a) >= 'A' && *(a) <= 'F')
|| (*(a) >= 'a' && *(a) <= 'f'))
#define xtoi_1(p) (*(p) <= '9'? (*(p)- '0'):
*(p) <= 'F'? (*(p)-'A'+10):(*(p)-'a'+10))
#define xtoi_2(p) ((xtoi_1(p) * 16) + xtoi_1((p)+1))
#define xmalloc(a) gcry_xmalloc ((a))
static void
show_sexp (const char *prefix, gcry_sexp_t a)
{
char *buf;
size_t size;
if (prefix)
fputs (prefix, stderr);
size = gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, NULL, 0);
buf = gcry_xmalloc (size);
gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, buf, size);
fprintf (stderr, "%.*s", (int)size, buf);
gcry_free (buf);
}
/* Convert STRING consisting of hex characters into its binary
representation and return it as an allocated buffer. The valid
length of the buffer is returned at R_LENGTH. The string is
delimited by end of string. The function returns NULL on
error. */
static void *
hex2buffer (const char *string, size_t *r_length)
{
const char *s;
unsigned char *buffer;
size_t length;
buffer = xmalloc (strlen(string)/2+1);
length = 0;
for (s=string; *s; s +=2 )
{
if (!hexdigitp (s) || !hexdigitp (s+1))
return NULL; /* Invalid hex digits. */
((unsigned char*)buffer)[length++] = xtoi_2 (s);
}
*r_length = length;
return buffer;
}
int main(int argc, char* argv[]) {
gpg_error_t err;
int algo;
gcry_md_hd_t hd;
unsigned char *tbs_ptr;
size_t len_tbs;
int i;
unsigned char *h;
gcry_sexp_t pub_key;
gcry_sexp_t data;
gcry_sexp_t sig;
gcry_mpi_t x;
int len_xy;
unsigned char c;
/*Для тестирования архитектуры little-endian или big-endian*/
unsigned short arch = 1; /* 0x0001 */
tbs_ptr = hex2buffer(tbc, &len_tbs);
if (tbs_ptr == NULL) {
fprintf (stderr, "Bad tbsn");
exit(1);
}
algo = gcry_md_map_name (hash_type);
if (algo == GCRY_MD_NONE)
{
fprintf (stderr, "Unknown algorithm '%s'n", hash_type);
exit (1);
}
err = gcry_md_open(&hd, algo, 0);
if (err)
{
fprintf (stderr, "LibGCrypt error %s/%sn",
gcry_strsource (err),
gcry_strerror (err));
exit (1);
}
gcry_md_write (hd, tbs_ptr, len_tbs);
h = gcry_md_read(hd, 0);
// len_xy = gcry_md_get_algo_dlen (algo);
/*Проверяем архитектуру и инвертируем хэш*/
printf("%sn", *((unsigned char *) &arch) == 0 ? "Архитектура big-endian" : "Архитекткра little-endian");
len_xy = *((unsigned char *) &arch) == 0 ? 0:gcry_md_get_algo_dlen (algo);
for (i = 0; i < (len_xy/2); i++) {
c = *(h + i);
*(h + i) = *(h + len_xy - i - 1);
*(h + len_xy - i - 1) = c;
}
fprintf(stderr, "Хэш длина=%dn", gcry_md_get_algo_dlen (algo));
for (i = 0; i < gcry_md_get_algo_dlen (algo); i++)
printf("%02X", h[i]);
// printf("n %sn", tbc);
fflush(stdout);
/*Сохраняем хэш */
x = gcry_mpi_set_opaque_copy(NULL, h, gcry_md_get_algo_dlen (algo) * 8);
/*Закрываем контекст хэширования*/
gcry_md_reset(hd);
gcry_md_close(hd);
/*Контекст хэша*/
err = gcry_sexp_build (&data, NULL,
"(data (flags gost) (value %m))", x);
show_sexp ("ECC GOST data cert:n", data);
fprintf (stderr, "nStep 1n");
/*Контекст публичного ключа корневого сертификата*/
err = gcry_sexp_sscan (&pub_key, NULL, pub_key_ca,
strlen (pub_key_ca));
if (err){
fprintf(stderr, "TEST_SEXP: er gcry_sexp_sscan for pub_key_can");
exit(1);
}
show_sexp ("ECC GOST public key:n", pub_key);
fprintf (stderr, "Step 2n");
/*Контекст подписи проверяемого сертификата*/
err = gcry_sexp_sscan (&sig, NULL, sig_cert, strlen (sig_cert));
if (err){
fprintf(stderr, "TEST_SEXP: er gcry_sexp_sscan for sig_certn");
exit(1);
}
show_sexp ("ECC GOST sig cert:n", sig);
fprintf (stderr, "Step 3n");
/*Проверка подписи сертификата*/
err = gcry_pk_verify (sig, data, pub_key);
if (err) {
fprintf (stderr, "TEST_SEXP: verify cert failedn");
exit (1);
}
fprintf (stderr, "TEST_SEXP: verify cert OK!!n");
}
Транслируем и выполняем утилиту TEST_from_TCL:
$cc –o TEST_from_TCL TEST_from_TCL.c –lgcrypt
$./TEST_from_TCL
Архитекткра little-endian
Хэш длина=32
D485903E7E8D60820118329060C558B9C733D53CA608C0C79363ECE7B4C1F799ECC GOST data cert:
(data
(flags gost)
(value #D485903E7E8D60820118329060C558B9C733D53CA608C0C79363ECE7B4C1F799#)
)
Step 1
ECC GOST public key:
(public-key
(ecc
(curve GOST2001-CryptoPro-A)
(q #040FA8E8C365FBA9792AF7293FF4838AC59BBE5B573164AD91D6C8231079BFA58FFBA1377303A17E90E
62EDC8462730D8BD5F93A70DA8A213FC85F915422C4755A#)
)
)
Step 2
ECC GOST sig cert:
(sig-val
(gost
(r #913931E75D87DD82EA58CACE2E321F54C26173E0D308D797598E9B84AE3F0D15#)
(s #A558A2452F6FB5C030F245DFC64F28ED8114195D98611FAAC7E6B223B7E58E76#)
)
)
Step 3
TEST_SEXP: verify cert OK!!
$
Как видим проверка подписи сертификата «УЦ 1 ИС ГУЦ.pem» прошла успешно. Осталось проверить сам корневой сертификат «ГУЦ Минкомсвязь.pem». Все просто, достаточно выполнить команду:
$ parse_certs_for_verify_load.tcl "ГУЦ Минкомсвязь.pem" "ГУЦ Минкомсвязь.pem" > TEST_from_TCL.h
$ и т.д.
Если кто захочет проверить сертификаты с другими ГОСТ-овыми ключами (ГОСТ Р 34.10-2012-256 или ГОСТ Р 34.10-2012-512), то может воспользоваться УЦ CAFL63 [6] и подготовить любые сертификаты:
[6]
И так, проделанные изыскания показали, что библиотека GCrypt вполне может использоваться для работы с российской криптографией. Ближайшая перспектива видится в доработке библиотеки KSBA и расширении пакета pki скриптового языка Tcl (а может и Python) поддержкой российской криптографии.
Те, кто хочет использовать библиотеку GCrypt для шифрования, рекомендую заглянуть сюда [10].
Автор: saipr
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/283194
Ссылки в тексте:
[1] «2000-й год»: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%B1%D0%BB%D0%B5%D0%BC%D0%B0_2000_%D0%B3%D0%BE%D0%B4%D0%B0
[2] исключена: https://habr.com/post/325666/
[3] GCrypt: https://habr.com/post/316736/
[4] ТК 26: https://tc26.ru/
[5] S-выражения: https://ru.wikipedia.org/wiki/S-%D0%B2%D1%8B%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5
[6] Image: https://habr.com/post/413493/
[7] Python: https://habr.com/post/335712/
[8] Tcl : https://habr.com/post/344526/
[9] TBS-сертификат: https://habr.com/post/194664/
[10] сюда: https://habr.com/post/120639
[11] Источник: https://habr.com/post/414249/?utm_campaign=414249
Нажмите здесь для печати.