imageТранспортный синтаксис ASN.1 определяет однозначный способ преобразования значений переменных допустимых типов в последовательность байт для передачи по сети. В ASN.1 он называется базовыми правилами кодирования (Basic Encoding Rules, BER). Правила являются рекурсивными, так что кодирование составных объектов представляет собой составление в цепочку закодированных последовательностей составляющих объектов. Протокол ASN.1 описывает структуру данных простым и понятным языком.

Каждое передаваемое значение — как базового, так и производного типа — состоит из трех полей:

  • идентификатор;
  • длина поля данных (в байтах);
  • поле данных.

Если всегда указывать длину поля данных (я считаю это правилом хорошего тона), то флаг конца поля данных не используется.

Есть много разных компиляторов для ASN.1, как платных, так и бесплатных, для разных языков программирования, но нам хотелось бы иметь под рукой что-то очень простое.

Подавляющее большинство разработчиков программ считают стандарт ASN.1 сложным. Я тоже так думал до недавнего времени. Работая в сфере ИОК/PKI/криптографии практически каждый день имеешь дело с ASN1-структурами в виде сертификатов X509, запросов на сертификаты, списки отозванных сертификатов. И этот список можно продолжать. И вот, работая над утилитой по созданию запроса на сертификат в формате PKCS#10 с генерацией ключевой пары на токене/смарткартк PKCS#11, мне, естественно, пришлось формировать, в частности, asn1-структуру публичного ключа для его записи в запрос на сертификат:

C-Sequence  
   C-Sequence  (<длина>)
      Object Identifier  (<длина>)
         <oid public key>
      C-Sequence  (<длина>)
         Object Identifier  (<длина>)
            <oid параметра подписи>
         Object Identifier  (<длина>)
            <oid хэша>
   Bit String  (<длина>)
	<значение публичного ключа>

Поскольку в качестве СКЗИ мы задействовали токен PKCS#11 с поддержкой российской криптографии, то исходный материал для данной структуры был получен с токена в соответствии со следующим шаблоном:

    CK_BYTE gostr3410par[12];
    CK_BYTE gostr3411par[12];
    CK_ULONG gostr3410par_len;
    CK_ULONG gostr3411par_len;
    CK_BYTE pubkey[128];
    CK_ULONG pubkeu_len;
    CK_KEY_TYPE key_type;
    CK_ATTRIBUTE templ_pk[] = {
	. . .
	    {CKA_GOSTR3410PARAMS, gostr3410par, sizeof(gostr3410par)},
	    {CKA_GOSTR3411PARAMS, gostr3411par, sizeof(gostr3410par)},
	    {CKA_VALUE, pubkey, sizeof(pubkey)},
	    {CKA_KEY_TYPE, &key_type, sizeof(key_type)}
    }

Напрямую из этой структуры для заполнения asn1-publickeyinfo будут задействованы значения атрибута CKA_VALUE, содержащее значение открытого ключа, и значения атрибутов CKA_GOSTR3410PARAMS и CKA_GOSTR3411PARAMS, которые содержат oid-ы параметра подписи и параметра хэша.

Атрибут CKA_KEY_TYPE, который может принимать значения CKK_GOSTR3410 и CKK_GOSTR3410_512 (в условиях когда алгоритм подписи ГОСТ Р 34.10-2001 продолжает действовать) неоднозначно определяет алгоритм ключевой пары. Если значение атрибута CKA_KEY_TYPE равно CKK_GOSTR3410_512, то, конечно, он одназначно указывает на алгоритм ГОСТ Р 34.10-2012 с длиной ключа в 512 бит (oid = 1.2.643.7.1.1.1.2). А вот если он равен просто CKK_GOSTR3410, то возникает двусмысленность, к какому типу ключа относится данный ключ: ГОСТ Р 34.10-2001 или все же это ГОСТ Р 34.10-2012 с длиной ключа 256 бит. Эту двусмысленность помогает разрешить атрибут CKA_GOSTR3411PARAMS.

Сразу отметим, что параметры CKA_GOSTR3410PARAMS и CKA_GOSTR3411PARAMS на токене в соответствии с рекомендациями ТК-26 хранятся в виде объектного идентификатора, закодированного oid-а, например:

\x06\x06\x2a\x85\x03\x02\x02\x13, где нулевой байт определяет тип последовательности (0x06 – объектный идентификатор, см. таблицу ниже), во втором байте указана длина (в общем случае длина может занимать несколько байт, но об этом ниже) поля данных, в котором хранится oid в бинарном виде.

Если этот параметр содержит oid алгоритма хэша ГОСТ Р 34.10-2012 с длиной 256 бита (oid=1.2.643.7.1.1.2.2, в бинарном виде "\x2a\x 85\x 03\x 07\x 01\x 01\x 02\x02" ), то тип ключа должен быть установлен как ГОСТ Р 34.10-2012 с длиной ключа 256 бит. В противном случае это ключ ГОСТ Р 34.10-2001. Алгоритм определения типа ключа может выглядеть так:

. . .
for (curr_attr_idx = 0; curr_attr_idx < (sizeof(templ_pk)/sizeof(templ_pk[0])); curr_attr_idx++){
  curr_attr = &templ_pk[curr_attr_idx];
  if (!curr_attr->pValue) {
	continue;
  }
  swith (curr_attr->type) { 
. . .
      case CKA_VALUE:
/*Длина публичного ключа*/
		pubkey_len = curr_attr->ulValueLen;
	break;
      case  CKA_GOSTR3410PARAMS:
/*Длина объектного идентификатора алгоритма подписи*/
		gostr3410par_len = curr_attr->ulValueLen;
	break;
      case  CKA_GOSTR3410PARAMS:
/*Длина объектного идентификатора хэш*/
		gostr3411par_len = curr_attr->ulValueLen;
	break;
      case CKA_KEY_TYPE:
	ulattr = curr_attr->pValue;
	if (*ulattr == CKK_GOSTR3410) {
		if (!memmem(gostr3411par), gostr3411par_len,"\x06\x08\x2a\x85\x03\x07", 6)) {
/*Тип ключа ГОСТ Р 34.10-2001*/
			strcpy(oid_key_type, "1.2.643.2.2.19");
			memcpy(oid_key_type_asn1("\x06\x06\x2a\x85\x03\x02\x02\x13", 8);
		} else {
/*Тип ключа ГОСТ Р 34.10-2012-256*/

			strcpy(oid_key_type,  ("1 2 643 7 1 1 1 1");
		            memcpy(oid_key_type_asn1 ("\x06\x08\x2a\x85\x03\x07\x01\x01\x01\x01", 10);
		}
	} else if (*ulattr == CKK_GOSTR3410_512) {
/*Тип ключа ГОСТ Р 34.10-2012-512*/
		strcpy(oid_key_type,  ("1 2 643 7 1 1 1 2");
		memcpy(oid_key_type_asn1 ("\x06\x08\x2a\x85\x03\x07\x01\x01\x01\x02", 10);
	} else {
fprintf(stderr, "tclpkcs11_perform_pki_keypair CKK_GOSTR ERROR\n");
			return (-1)
	}
	break;
. . .
  }
}
. . .

Теперь у нас есть все исходные данные для создания asn1- структуры публичного ключа.

Напомним, что каждый элемент asn1-структуры состоит из трех полей:

  • идентификатор;
  • длина поля данных (в байтах);
  • поле данных.

Приведем таблицу кодирования некоторых типов идентификаторов, используемых в ИОК/PKI:

Наименование типа Краткое описание Представление типа в DER-кодировке
SEQUENCE Используется для описания структуры данных, состоящей из различных типов. 30
INTEGER Целое число. 02
OBJECT IDENTIFIER Последовательность целых чисел. 06
UTCTime Временной тип, содержит 2 цифры для определения года 17
GeneralizedTime Расширенный временной тип, содержит 4 цифры для обозначения года. 18
SET Описывает структуру данных разных типов. 31
UTF8String Описывает строковые данные. 0C
NULL Собственно NULL 05
BIT STRING Тип для хранения последовательности бит. 03
OCTET STRING Тип для хранения последовательности байт 04

При работе с asn1-структурами наибольший шок для непосвященных вызывает метод кодирования длины поля данных, особенно при его формировании, да если еще учитывать архитектуру компьютера (littleendien, bigendien). Это целая наука. И вот в процессе рассуждения об алгоритме формирования этого поля, на ум пришла мысль использовать функцию sprintf, которая сама будет учитывать архитектуру, а как определяется количество байт для хранения длины видно по коду функции, которая готовит буфер с идентификатором типа данных и длиной данных:

unsigned char *wrap_id_with_length(unsigned char type, //тип данных
unsigned long length, //Длина данных
unsigned long *lenasn) //Возврат длины asn1-структуры
{
//    unsigned long length;
    int buflen = 0;
    unsigned char *buf;
    char *format;
    char *buf_for_len[100];
    const char *s;
/*Формат вывода заголовка в зависимости от длины данных*/
    char f0[] = "%02x%02x";
    char f1[] = "%02x81%02x";
    char f2[] = "%02x82%04x";
    char f3[] = "%02x83%06x";
    char f4[] = "%02x84%08x";
/*Определяем длину буфера для типа и длины данных */
    buflen = ( length < 0x80 ? 1:
		length <= 0xff ? 2:
        		length <= 0xffff ? 3:
                	length <= 0xffffff ? 4: 5);
/*Выделяем буфер для asn-структуры*/
    buf = malloc(length + buflen);
//    buf = malloc(buflen);
/*В зависимости от длины данных выбираем формат для sprintf*/
    switch (buflen - 1) {
	case 0:
	    format = f0;
	    break;
	case 1:
	    format = f1;
	    break;
	case 2:
	    format = f2;
	    break;
	case 3:
	    format = f3;
	    break;
	case 4:
	    format = f4;
	    break;
    }
//Через sprintf  мы решаем проблемы little и bigendian и упаковываем тип поля и длину
    sprintf((char*)buf_for_len, (const char *)format, type, length);
    length = 0;
/*Печатаем asn1-заголовок*/
    fprintf(stderr, "ASN1 - заголовок:%s\n", buf_for_len);
/*Из шестнадцатеричного вида в бинарный вид*/
    for (s=(const char *)buf_for_len; *s; s +=2 )
    {
      if (!hexdigitp (s) || (!hexdigitp (s+1) && hexdigitp (s+1) != 0) ){
        fprintf (stderr, "invalid hex digits in \"%s\"\n", buf_for_len);
        *lenasn = 0;
        return NULL;
      }
      ((unsigned char*)buf)[length++] = xtoi_2 (s);
    }
    *lenasn = length;
    return (buf);
}

Функция возвращает указатель на буфер с asn1-структурой, выделенный с учетом длины данных. Осталось эти данные скопировать в полученный буфер со смещением на длину заголовка. Длина заголовка возвращается через параметр lenasn.

Для того, чтобы проверить как работает эта функция, напишем простую утилиту:

#include <stdio.h>
#include <stdlib.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))
int main (int argc, char *argv[]) {
    unsigned char *hdrasn;
    unsigned char type;
    unsigned long length;
    unsigned long lenasn;
    if (argc != 3) {
        fprintf (stderr, "Usage: wrap_id_with_length <id> <length>\n");
        exit(-1);
    }
    type = atoi(argv[1]);
    length = atol(argv[2]);
fprintf (stderr, "<id=%02x> <length=%lu>\n", type, length);
    if (length == 0) {
        fprintf (stderr, "Bad length=%s\nUsage: wrap_id_with_length <id> <length>\n", argv[2]);
        exit(-1);
    }
    hdrasn = wrap_id_with_length(type, length, &lenasn);
    fprintf (stderr, "Length asn1-buffer=%lu, LEN_HEADER=%lu, LEN_DATA=%lu\n", lenasn,                           lenasn - length, length);
}

Сохраним ее вместе с функцией wrap_id_with_length в файле wrap_id_with_length.c.

Оттранслируем:

$cc –o wrap_id_with_length wrap_id_with_length.c
$

Полученную программу запустим с различными исходными данными. Тип данных задается десятичным числом.

Полученную программу запустим с различными исходными данными. Тип данных задается десятичным числом:

bash-4.3$ ./wrap_id_with_length  06 8 
<id=06> <length=8> 
ASN1 - заголовок:0608 
Length asn1-buffer=10, LEN_HEADER=2, LEN_DATA=8 
bash-4.3$ ./wrap_id_with_length  06 127 
<id=06> <length=127> 
ASN1 - заголовок:067f 
Length asn1-buffer=129, LEN_HEADER=2, LEN_DATA=127 
bash-4.3$ ./wrap_id_with_length  48 128 
<id=30> <length=128> 
ASN1 - заголовок:308180 
Length asn1-buffer=131, LEN_HEADER=3, LEN_DATA=128 
bash-4.3$ ./wrap_id_with_length  48 4097 
<id=30> <length=4097> 
ASN1 - заголовок:30821001 
Length asn1-buffer=4101, LEN_HEADER=4, LEN_DATA=4097 
bash-4.3$

Проверить правильность формирования заголовка можно с помощью любого калькулятора:



Все мы готовы формировать любую ASN1-структуру. Но прежде внесем небольшие изменения в функцию wrap_id_with_length и назовем ее

wrap_for_asn1:
unsigned char *wrap_for_asn1(unsigned char type, unsigned char *prefix, unsigned long prefix_len,  unsigned char *wrap, unsigned long wrap_len, unsigned long *lenasn){
    unsigned long length;
    int buflen = 0;
    unsigned char *buf;
    char *format;
    const char buf_for_len[100];
    const char *s;
    char f0[] = "%02x%02x";
    char f1[] = "%02x81%02x";
    char f2[] = "%02x82%04x";
    char f3[] = "%02x83%06x";
    char f4[] = "%02x84%08x";
    length = prefix_len + wrap_len;
    buflen += ( length <= 0x80 ? 1:
		length <= 0xff ? 2:
        	length <= 0xffff ? 3:
                length <= 0xffffff ? 4: 5);
    buf = malloc(length + buflen);
    switch (buflen - 1) {
	case 0:
	    format = f0;
	    break;
	case 1:
	    format = f1;
	    break;
	case 2:
	    format = f2;
	    break;
	case 3:
	    format = f3;
	    break;
	case 4:
	    format = f4;
	    break;
    }
//Через sprintf  мы решаем проблемы little и bigendian и вычисляем длину
    sprintf((char*)buf_for_len, (const char *)format, type, length);
    length = 0;
    for (s=buf_for_len; *s; s +=2 )
    {
      if (!hexdigitp (s) || (!hexdigitp (s+1) && hexdigitp (s+1) != 0) ){
        fprintf (stderr, "invalid hex digits in \"%s\"\n", buf_for_len);
      }
      ((unsigned char*)buf)[length++] = xtoi_2 (s);
    }
    if (prefix_len > 0) {
	memcpy(buf + length, prefix, prefix_len);
    }
    memcpy(buf + length + prefix_len, wrap, wrap_len);
    *lenasn = (unsigned long)(length + prefix_len + wrap_len);
    return (buf);
}


Как видно, изменения минимальные. В качестве входных параметров добавлены сами данные, которые внутри функции пакуются в asn1-структуру. Причем, на вход можно подавать сразу два буфера. Это, как нам кажется, удобно.

Прежде чем представить контрольный пример дадим коды еще трех функций. Первая функция oid2buffer преобразует oid-ы из точечно-десятичной формы в DER-кодировку. Нам эта функция потребуется для преобразования, в частности, oid-ов ключевой пары (см.выше).

Текст функции находится здесь:
static char *oid2buffer(char* oid_str, unsigned long *len){
char *curstr;
char *curstr1;
char *nextstr;
unsigned int firstval;
unsigned int secondval;
unsigned int val;
unsigned char buf[5];
int count;
unsigned char oid_hex[100];
char *res;
int i;
if ( oid_str == NULL ) {
*len = 0;
return NULL;
}
*len = 0;
curstr = strdup((const char*)oid_str);
curstr1 = curstr;
nextstr = strchr(curstr, '.');
if ( nextstr == NULL ) {
*len = 0;
return NULL;
}
*nextstr = '\0';
firstval = atoi(curstr);
curstr = nextstr + 1;
nextstr = strchr(curstr, '.');
if ( nextstr ) {
*nextstr = '\0';
}
secondval = atoi(curstr);
if ( firstval > 2 ) {
*len = 0;
return NULL;
}
if ( secondval > 39 ) {
*len = 0;
return NULL;
}
oid_hex[0] = (unsigned char)(( firstval * 40 ) + secondval);
i = 1;
while ( nextstr ) {
curstr = nextstr + 1;

nextstr = strchr(curstr, '.');

if ( nextstr ) {
*nextstr = '\0';
}

memset(buf, 0, sizeof(buf));
val = atoi(curstr);
count = 0;
if(curstr[0] != '0')
while ( val ) {
buf[count] = ( val & 0x7f );
val = val >> 7;
count++;
}
else{
buf[count] = ( val & 0x7f );
val = val >> 7;
count++;
}
while ( count-- ) {
if ( count ) {
oid_hex[i] = buf[count] | 0x80;
} else {
oid_hex[i] = buf[count];
}
i++;
}
}
res = (char*) malloc(i);
if(res){
memcpy(res, oid_hex, i);
*len = i;
}
free(curstr1);
return res;
}

Две остальные функции позволяют бинарный буфер преобразовать в шестнадцатеричный кол (buffer2hex) и обратно (hex2buffer).

Эти функции находятся здесь:
static char*
buffer2hex (const unsigned char *src, size_t len)
{
int i;
char *dest;
char *res;
dest = (char *)malloc(len * 2 + 1);
res = dest;
if (dest)
{
for (i=0; i < len; i++, dest += 2 )
sprintf (dest, "%02X", src[i]);
}
return res;
}

static void *
hex2buffer (const char *string, size_t *r_length)
{
const char *s;
unsigned char *buffer;
size_t length;

buffer = malloc (strlen(string)/2+1);
length = 0;
for (s=string; *s; s +=2 )
{
if (!hexdigitp (s) || !hexdigitp (s+1)){
fprintf (stderr, «invalid hex digits in \»%s\"\n", string);
}
((unsigned char*)buffer)[length++] = xtoi_2 (s);
}
*r_length = length;
return buffer;
}

Эти функции очень удобны при отладке и наверняка у многих они есть.

И вот теперь возвращаемся к решению поставленной задачи, получению asn1-структуры публичного ключа. Напишем утилиту, которая сформирует и сохранит в файле ASN1_PIBINFO.der asn1-структуру публичного ключа.

Эта утилита находится здесь:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <string.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))
/*Вставьте код функции oid2buffer*/
/*Вставьте код функций buffer2hex и hex2buffer*/
/*Вставьте код функции wrap_for_asn1*/
int main() {
    int fd;
    unsigned char *asn, *asn1, *asn2, *asn3, *pubkeyalgo;
    unsigned char* pubkey_bin;
//Исходные данные
    char gost3410par[] = "\x06\x7\x2a\x85\x03\x02\x02\x23\x01";
    unsigned long gost3410par_len =  sizeof(gost3410par) - 1;
    char gost3411par[] = "\x06\x8\x2a\x85\x03\x07\x01\x01\x02\x02";
    unsigned long gost3411par_len =  sizeof(gost3411par) - 1;
    unsigned char pubkey_hex[] = "9af03570ed0c54cd4953f11ab19e551022cd48603326c1b9b630b1cff74e5a160ba1718166cc22bf70f82bdc957d924c501b9332491cb3a36ce45770f05487b5";
    char pubkey_oid_2001[] = "1.2.643.2.2.19";
    char pubkey_oid_2012_256[] = "1.2.643.7.1.1.1.1";
    char pubkey_oid_2012_512[] = "1.2.643.7.1.1.1.2";

    unsigned long pubkey_len, pubkey_len_full, len10, len11, len12, lenalgo;
    unsigned char *pkalgo; 
    unsigned long pkalgo_len;

  uint16_t x = 1; /* 0x0001 */
  printf("%s\n", *((uint8_t *) &x) == 0 ? "big-endian" : "little-endian");

////pubkeyinfo
//Определяем тип ключа по алгоритмы хэш
    if (!memmem(gost3411par, 8, "\x2a\x85\x03\x07", 4)) {
//хэш ГОСТ Р 34.11-94, тип ключа ГОСТ Р 34.10-2001 - 1.2.643.2.2.19
	pubkeyalgo = (unsigned char *)oid2buffer(pubkey_oid_2001, &lenalgo);
    } else if (!memcmp(gost3411par, "\x2a\x85\x03\x07\x01\x01\x02\x02", 8)){
//хэш ГОСТ Р 34.11-2012-256, тип ключа ГОСТ Р 34.10-2012-256 - 1.2.643.7.1.1.1.1
	pubkeyalgo = (unsigned char *)oid2buffer(pubkey_oid_2012_256, &lenalgo);
    } else {
//хэш ГОСТ Р 34.11-2012-512, тип ключа ГОСТ Р 34.10-2012-512 - 1.2.643.7.1.1.1.2
	pubkeyalgo = (unsigned char *)oid2buffer(pubkey_oid_2012_512, &lenalgo);
    }

    pubkey_bin =(unsigned char*)hex2buffer((const char *)pubkey_hex, &pubkey_len);
//Упаковываем значение публичного ключа
    asn1 = wrap_for_asn1_bin('\x04', (unsigned char *)"", 0, pubkey_bin, pubkey_len, &pubkey_len);
    asn = wrap_for_asn1_bin('\x03',  (unsigned char *)"\x00", 1, asn1, pubkey_len, &pubkey_len_full);
fprintf(stderr, "PUBLIC_VALUE=%s\n", buffer2hex(asn, pubkey_len_full));
    free(asn1);

//Упаковываем параметры
    asn3 = wrap_for_asn1_bin('\x30', (unsigned char*)gost3410par, gost3410par_len, (unsigned char *)gost3411par, gost3411par_len, &len12);
fprintf(stderr, "\nPARAMS len12=%lu, FULL=%s\n", len12, buffer2hex(asn3, len12));
//Упаковываем тип ключа
    pkalgo = wrap_for_asn1_bin('\x06', (unsigned char *)"", 0,  pubkeyalgo, lenalgo, &pkalgo_len);
//Упаковываем тип ключа с параметрами
    asn2 = wrap_for_asn1_bin('\x30',  pkalgo, pkalgo_len, asn3, len12, &len11);
fprintf(stderr, "PubKEY=%s\n", buffer2hex(asn3, len11));

    asn1 = wrap_for_asn1_bin('\x30',  asn2, len11, asn, pubkey_len_full, &len10);
    free(asn2); free(asn3);
fprintf(stderr, "\n%s\n", buffer2hex(asn1, len10));
    fd = open ("ASN1_PUBINFO.der", O_TRUNC|O_RDWR|O_CREAT,S_IRWXO);
    write(fd, asn1, len10);
    close(fd);
    free(asn1);
    chmod("ASN1_PUBINFO.der", 0666); 
}


Для проверки результата воспользуемся утилитами derdump и pp из состава пакета NSS.

Первая утилита нам покажет asn1-структуру публичного ключа:

$ derdump -i ASN1_PUBINFO.der  
C-Sequence  (102) 
  C-Sequence  (31) 
     Object Identifier  (8) 
        1 2 643 7 1 1 1 2 (GOST R 34.10-2012 Key 512) 
     C-Sequence  (19) 
        Object Identifier  (7) 
           1 2 643 2 2 35 1  
        Object Identifier  (8) 
           1 2 643 7 1 1 2 2 (GOST R 34.11-2012 256) 
  Bit String  (67) 
     00 04 40 9a f0 35 70 ed 0c 54 cd 49 53 f1 1a b1 9e 55 10 22 cd 48  
     60 33 26 c1 b9 b6 30 b1 cf f7 4e 5a 16 0b a1 71 81 66 cc 22 bf 70  
     f8 2b dc 95 7d 92 4c 50 1b 93 32 49 1c b3 a3 6c e4 57 70 f0 54 87  
     b5
$

Вторая покажет содержание ключа:

$ pp -t pk -i  ASN1_PUBINFO.der
Public Key: 
   Subject Public Key Info: 
       Public Key Algorithm: GOST R 34.10-2012 512 Public Key: 
           PublicValue: 
               9a:f0:35:70:ed:0c:54:cd:49:53:f1:1a:b1:9e:55:10: 
               22:cd:48:60:33:26:c1:b9:b6:30:b1:cf:f7:4e:5a:16: 
               0b:a1:71:81:66:cc:22:bf:70:f8:2b:dc:95:7d:92:4c: 
               50:1b:93:32:49:1c:b3:a3:6c:e4:57:70:f0:54:87:b5 
           GOSTR3410Params: OID.1.2.643.2.2.35.1 
           GOSTR3411Params: GOST R 34.11-2012 256
$

Желающие могут перепроверить, например утилитой openssl желательно с подключенным ГОСТ-овым engine:

$ /usr/local/lirssl_csp_64/bin/lirssl_static  asn1parse -inform DER -in ASN1_PUBINFO.der  
   0:d=0  hl=2 l= 102 cons: SEQUENCE           
   2:d=1  hl=2 l=  31 cons: SEQUENCE           
   4:d=2  hl=2 l=   8 prim: OBJECT            :GOST R 34.10-2012 with 512 bit modulus 
  14:d=2  hl=2 l=  19 cons: SEQUENCE           
  16:d=3  hl=2 l=   7 prim: OBJECT            :id-GostR3410-2001-CryptoPro-A-ParamSet 
  25:d=3  hl=2 l=   8 prim: OBJECT            :GOST R 34.11-2012 with 256 bit hash 
  35:d=1  hl=2 l=  67 prim: BIT STRING         
$

Как видим, полученная ASN1структура везде успешно проходит проверку.

Предложенный алгоритм и утилита формирование asn1-структур не требует использования никаких ASN1-компиляторов и доролнительных библиотек (той же openssl) и оказались очень удобными в использовании. Мы их еще вспомним в следующей статье, когда исполнится пожелание Pas и будет представлена графическая утилита, делающая не только «парсинг сертификатов» и проверку их валидности, но и генерирующая ключевую пару на токенах PKCS#11, формирующая и подписывающая запрос на квалифицированным сертификат. С этим запросом можно смело отправляться на УЦ за сертификатом. Опережая вопросы, сразу отмечу, что в последнем случае токен должен быть сертифицирован как СКЗИ в системе сертификации ФСБ России.

Комментарии (20)


  1. Scratch
    31.03.2019 22:37
    +1

    Подавляющее большинство разработчиков программ считают стандарт ASN.1 сложным. Я тоже так думал до недавнего времени.

    Статья только усугубила это мнение. И это еще без explicit/implicit и кастомных тегов.
    Советую всем утилиту для просмотра ASN.1 структур — lapo.it/asn1js.очень помогает при дебаге



    1. stargrave2
      01.04.2019 11:46

      habr.com/ru/post/444272 — а вот тут pure Python библиотека на работы с BER/DER, состоящая из одного файла. С ASN.1 можно работать и удобно.


      1. saipr Автор
        01.04.2019 12:05

        Есть много разных компиляторов для ASN.1, как платных, так и бесплатных, для разных языков программирования, но нам хотелось бы иметь под рукой что-то очень простое.

        А вот и ваша цитата:


        Вообще-то рекомендовать ASN.1 для криптографических задач не стоит: ASN.1 и его кодеки — сложны.

        И вы предлагаете тащить Python в Си?


        Предложенный алгоритм и утилита формирование asn1-структур не требует использования никаких ASN1-компиляторов и доролнительных библиотек (той же openssl)

        для создания ASN1-структур.


        1. stargrave2
          01.04.2019 12:52
          +1

          Python в C конечно тащить не стоит. Мой комментарий исключительно чтобы показать что с ASN.1 не всегда страшно работать.


          1. saipr Автор
            01.04.2019 13:22

            Тут я с вами полностью согласен.


  1. kovserg
    01.04.2019 02:20

    Раз уж пошла такая пьянка. Вопрос эксперту если у нас однозначный способ то почему я могу задать приватный ключ двумя спосабами?

    Вариант 1

      /*00*/  0x30,70,
      /*02*/    2,1,0, // Integer(0)
      /*05*/    0x30,31, 
      /*07*/      6,8, 0x2A,0x85,3, 7,1,1,1,1, // 1.2.643.7.1.1.1.1
      /*17*/      0x30,19,
      /*19*/        6,7, 42,0x85,3, 2,2,36,0,  // 1.2.643.2.2.36.0
      /*28*/        6,8, 42,0x85,3, 7,1,1,2,2, // 1.2.643.7.1.1.2.2
      /*38*/    4,32, // OctetString
      /*40*/          1,2,3,0,0,0,0,0,0,0,0,0,0,0,0,0,
      /*56*/          0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    

    Вариант 2
      /*00*/  0x30,72,
      /*02*/    2,1,0, // Integer(0)
      /*05*/    0x30,31, 
      /*07*/      6,8, 0x2A,0x85,3, 7,1,1,1,1, // 1.2.643.7.1.1.1.1
      /*17*/      0x30,19,
      /*19*/        6,7, 42,0x85,3, 2,2,36,0,  // 1.2.643.2.2.36.0
      /*28*/        6,8, 42,0x85,3, 7,1,1,2,2, // 1.2.643.7.1.1.2.2
      /*38*/    4,34, 2,32 // OctetString int
      /*42*/          0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
      /*58*/          0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,1
    

    Но байты ключа будут отличаться порядком следования (прямой и задом наперёд)


    1. saipr Автор
      01.04.2019 10:09

      Значение приватного ключа это данные. Ваши данные (кстати, я надеюсь вы его не из PKCS#11 неизвлекаемого вытащили) и как вы их положите так и будет. В данном случае вы должны учитывать архитектуру вашего компьютера (bigendien, little endien) и, опираясь на соответствующий документ (если речь идет о ГОСТ-ах, то это рукомендации ТК-26), кодировать ваши данные. В данном случае знвчение приватного ключа. Привожу код для определения архитектуры машины (он, кстати, есть в коде процедуры main — забыл убрать):


       uint16_t x = 1; /* 0x0001 */
        printf("%s\n", *((uint8_t *) &x) == 0 ? "big-endian" : "little-endian");

      Итак, данные это ваша зона ответственности.


      1. kovserg
        01.04.2019 10:18

        Данные я вытащил из openssl он сгенерировал закрытый ключ в таком виде

        openssl genpkey -algorithm gost2012_256 -pkeyopt paramset:XA -out priv.key

        при этом оба варианта работают с int внутри и без. Вызывает некое беспокойство что если приватный ключ (вдруг) будет начинаться с байта 0x02.


        1. saipr Автор
          01.04.2019 10:45

          У вас два разных файла, в которых один и тотже приватный ключ упакован по разному.
          В первом случае это просто octetstring:


          /38/ 4,32, // OctetString
          /40/ 1,2,3,0,0,0,0,0,0,0,0,0,0,0,0,0,
          /56/ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

          Во втором случае это int завернутый в octetstring:


          /38/ 4,34, 2,32 // OctetString int
          /42/ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
          /58/ 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,1

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


          1. kovserg
            01.04.2019 13:05
            +1

            Чем это страшно, если это просто тестовые ключи для опытов.
            Страшно то во что превратили цифровую подпись — в механизм развода на деньги при любом чихе. Вот например сегодня очередной подарок от клиента Россельхозбанк-а прилетел, празднуйте 1-ое апреля же.


        1. saipr Автор
          01.04.2019 11:01

          Вот что по этому поводу написано в документе ТК-26 "Транспортныйконтейнер
          ключевой. Дополнения к PKCS#8 и PKCS#12.Версия 2.0"


          Поскольку размерность типа INTEGER может изменяться при наличии
          нулевых значений в старших разрядах числа, использование такого типа для
          представления закрытого ключа создает неудобства при вычислении размерности.
          Предлагается использовать для хранения значений ключа OCTET
          STRING, то есть:
          GostR3410-2012-KeyValueMask ::= OCTET STRING { KM |M1|M2|....|Mk }
          или
          GostR3410-2012-KeyValueInfo :: = SEQUENCE{
          GostR3410-2012-KeyValueMask,
          GostR3410-2012-PublicKey }
          GostR3410-2012-PublicKey :: = OCTET STRING {PubKeyX|PubKeyY}
          ...
          Для обеспечения унификации между представлениями ключей в PFX и
          сертификатах формата X.509, как секретный, так и открытый ключи
          представляются в формате little-endian (старший байт справа).

          Итак, первый ваш файл с приватным ключом соответствует рекомендациям ТК-26, а второй это всеже самодеятельность.


          1. kovserg
            01.04.2019 13:10

            А в этом документе ничего не написано про OID 1.2.840.113549.1.12.1.80?


            1. saipr Автор
              01.04.2019 13:21

              А в этом документе ничего не написано про OID 1.2.840.113549.1.12.1.80?

              Нет, конечно. Этого oid-а 1.2.840.113549.1.12.1.80 нет (по крайней мере я не видел) в документах ТК-26 (а значит и ФСБ). Я так понимаю вы воюите с PFX/P12 аля КриптоПро? Посмотрите это.


              1. kovserg
                01.04.2019 14:06

                Не этот (cd-ejector) денег хочет. И с cryptopro я не вою, но ключи пришлось отобрать.


                1. saipr Автор
                  01.04.2019 14:20

                  Воюете в смысле разбираетеся. И закрытый ключ ГОСТ Р 34.10-2012-512 тоже отбираете?


                  1. kovserg
                    01.04.2019 14:24

                    Нет. Я по мере возникновения проблем. Пока 512 не требовалось. Но если для выгрузки списков запрещенных сайтов понадобиться 512 — добавлю.


  1. vlsinitsyn
    01.04.2019 12:44

    ASN.1 — годный протокол. Пакует более эффективно чем имеющиеся альтернативы (например протобуф). А все потому, что позволяет более детально описывать типы и соответственно более эффективно их паковать.
    Но проблема с отсутствием надежного бесплатного компилятора. Платных компиляторов индустриального уровня всего два. И они не дешевые. И проблема с их использованием даже не в том, чтобы купить в вашей организации. Проблема, что если задействовать протокол, то все партнеры должны будут тоже купить. Потому что «в ручную» писать разбор — это дорого и больно. И поэтому — протобуф.


  1. dkokarev
    01.04.2019 18:24

    Пожалуй самый лучший и тому же бесплатный ASN1 парсер/генератор: github.com/vlm/asn1c
    Мы его активно используем для разбора 3GPP и прочих около-телефонных записей.
    Раньше пользовали платный Obj-Sys, и это просто ужас.


    1. saipr Автор
      01.04.2019 18:26

      Имеет право на жизнь. Насчет лучшего… И чем он лучше, кроме бесплатного. ?