
La sintaxis de transporte ASN.1 define una forma única de convertir valores de variables de tipos válidos en una secuencia de bytes para su transmisión a través de una red. En ASN.1, se llama Reglas básicas de codificación (BER). Las reglas son recursivas, por lo que codificar objetos compuestos es un encadenamiento de secuencias codificadas de objetos componentes. El protocolo ASN.1 describe la estructura de datos en un
lenguaje simple y comprensible .
Cada valor transmitido, tanto de tipo básico como derivado, consta de tres campos:
- identificador
- longitud del campo de datos (en bytes);
- campo de datos
Si siempre especifica la longitud del campo de datos (lo considero una buena práctica), no se utiliza el indicador del final del campo de datos.
Hay muchos compiladores diferentes para ASN.1, tanto de pago como gratuitos, para diferentes lenguajes de programación, pero nos gustaría tener algo muy simple a mano.
La gran mayoría de los desarrolladores de software encuentran el
complejo estándar
ASN.1 . Yo también lo pensaba hasta hace poco. Trabajando en el campo de PKI / PKI / criptografía casi todos los días se trata de estructuras ASN1 en forma de certificados X509, solicitudes de certificados, listas de certificados revocados. Y la lista continúa. Y así, mientras trabajaba en una utilidad para crear una solicitud de certificado en formato PKCS # 10 con la generación de un par de claves en un token / tarjeta inteligente PKCS # 11, naturalmente tuve que formar, en particular, la estructura asn1 de la clave pública para escribirla en la solicitud de certificado :
C-Sequence C-Sequence (<>) Object Identifier (<>) <oid public key> C-Sequence (<>) Object Identifier (<>) <oid > Object Identifier (<>) <oid > Bit String (<>) < >
Como utilizamos el token PKCS # 11 con soporte para la criptografía rusa como una herramienta de protección de la información criptográfica, el material fuente para esta estructura se obtuvo del token de acuerdo con la siguiente plantilla:
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)} }
Directamente desde esta estructura, los valores del atributo CKA_VALUE que contiene el valor de la clave pública y los valores de los atributos CKA_GOSTR3410PARAMS y CKA_GOSTR3411PARAMS que contienen los oids del parámetro de firma y el parámetro hash se usarán para completar asn1-publickeyinfo.
El atributo CKA_KEY_TYPE, que puede tomar los valores CKK_GOSTR3410 y CKK_GOSTR3410_512 (en condiciones en que el algoritmo de firma GOST R 34.10-2001 continúa funcionando) define de manera ambigua el algoritmo de par de claves. Si el valor del atributo CKA_KEY_TYPE es CKK_GOSTR3410_512, entonces, por supuesto, apunta únicamente al algoritmo GOST R 34.10-2012 con una longitud de clave de 512 bits (oid = 1.2.643.7.1.1.1.2). Pero si es simplemente igual a CKK_GOSTR3410, entonces existe ambigüedad sobre a qué tipo de clave pertenece esta clave: GOST R 34.10-2001 o aún es GOST R 34.10-2012 con una longitud de clave de 256 bits. El atributo CKA_GOSTR3411PARAMS ayuda a resolver esta ambigüedad.
Inmediatamente, observamos que los parámetros CKA_GOSTR3410PARAMS y CKA_GOSTR3411PARAMS en el token de acuerdo con las recomendaciones de TK-26 se almacenan en forma de un identificador de objeto codificado por oid, por ejemplo:
\ x06 \ x06 \ x2a \ x85 \ x03 \ x02 \ x02 \ x13, donde el byte cero determina el tipo de secuencia (0x06 es el identificador de objeto, vea la tabla a continuación), el segundo byte indica la longitud (en el caso general, la longitud puede tomar varios bytes, pero más sobre eso a continuación) el campo de datos en el que oid se almacena en forma binaria.
Si este parámetro contiene el oid del algoritmo hash GOST R 34.10-2012 con una longitud de 256 bits (oid = 1.2.643.7.1.1.2.2, en forma binaria "\ x2a \ x 85 \ x 03 \ x 07 \ x 01 \ x 01 \ x 02 \ x02 "), entonces el tipo de clave debe establecerse como GOST R 34.10-2012 con una longitud de clave de 256 bits. De lo contrario, es la clave de GOST R 34.10-2001. El algoritmo para determinar el tipo de clave puede verse así:
. . . 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)) { 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 { 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) { 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; . . . } } . . .
Ahora tenemos todos los datos de origen para crear la estructura de clave pública asn1.
Recuerde que cada elemento de la estructura asn1 consta de tres campos:
- identificador
- longitud del campo de datos (en bytes);
- campo de datos
Aquí hay
una tabla de codificación para algunos tipos de identificadores utilizados en PKI / PKI:
Nombre de tipo | Breve descripción | Representación de tipo en codificación DER |
---|
SECUENCIA | Se usa para describir una estructura de datos que consta de varios tipos. | 30 |
INTEGER | Entero | 02 |
IDENTIFICADOR DE OBJETO | Una secuencia de enteros. | 06 |
UTCTime | Tipo temporal, contiene 2 dígitos para determinar el año. | 17 |
Tiempo generalizado | Tipo de tiempo extendido, contiene 4 dígitos para indicar el año. | 18 años |
SET | Describe la estructura de datos de varios tipos. | 31 |
UTF8String | Describe datos de cadena. | 0C |
Nulo | En realidad NULL | 05 |
BIT STRING | Tipo para almacenar una secuencia de bits. | 03 |
OCTET STRING | Tipo para almacenar una secuencia de bytes | 04 |
Cuando se trabaja con estructuras asn1, el mayor impacto para los no iniciados es el método de codificación de la longitud del campo de datos, especialmente cuando está formado, y si tenemos en cuenta la arquitectura de la computadora (littleendien, bigendien). Esta es toda una
ciencia . Y así, en el proceso de discutir el algoritmo para formar este campo, se me ocurrió la idea de usar la función sprintf, que tendrá en cuenta la arquitectura, y cómo el código de función puede ver la cantidad de bytes para almacenar la longitud, que prepara un búfer con el identificador de tipo de datos y la longitud de los datos:
unsigned char *wrap_id_with_length(unsigned char type,
La función devuelve un puntero a un búfer con una estructura asn1, asignada teniendo en cuenta la longitud de los datos. Queda por copiar estos datos en el búfer recibido con un desplazamiento por la longitud del encabezado. La longitud del encabezado se devuelve a través del parámetro lenasn.
Para verificar cómo funciona esta función, escribiremos una utilidad simple:
#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); }
Guárdelo junto con la función wrap_id_with_length en el archivo wrap_id_with_length.c.
Transmitimos:
$cc –o wrap_id_with_length wrap_id_with_length.c $
Ejecute el programa resultante con varios datos de origen. El tipo de datos se especifica mediante un número decimal:
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$
Puede verificar la corrección de la formación del encabezado utilizando cualquier calculadora:

Todos estamos listos para formar cualquier estructura ASN1. Pero primero, realice pequeños cambios en la función wrap_id_with_length y llámela
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; }
Como puede ver, los cambios son mínimos. Como parámetros de entrada, se agregan los datos en sí, que dentro de la función se empaquetan en una estructura asn1. Además, se pueden alimentar dos memorias intermedias a la entrada a la vez. Nos parece conveniente.
Antes de presentar un caso de prueba, le damos los códigos de tres funciones más. La primera función
oid2buffer convierte oid s de forma decimal punteada a codificación DER. Necesitamos esta función para convertir, en particular, los oidos del par de claves (ver arriba).
El texto de la función está aquí: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 cuenta;
unsigned char oid_hex [100];
char * res;
int i;
if (oid_str == NULL) {
* len = 0;
devuelve NULL;
}
* len = 0;
curstr = strdup ((const char *) oid_str);
curstr1 = curstr;
nextstr = strchr (curstr, '.');
if (nextstr == NULL) {
* len = 0;
devuelve NULL;
}
* nextstr = '\ 0';
firstval = atoi (curstr);
curstr = nextstr + 1;
nextstr = strchr (curstr, '.');
if (nextstr) {
* nextstr = '\ 0';
}
secondval = atoi (curstr);
if (firstval> 2) {
* len = 0;
devuelve NULL;
}
if (segundo valor> 39) {
* len = 0;
devuelve NULL;
}
oid_hex [0] = (carácter sin signo) ((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);
cuenta = 0;
if (curstr [0]! = '0')
mientras que (val) {
buf [cuenta] = (val & 0x7f);
val = val >> 7;
recuento ++;
}
más {
buf [cuenta] = (val & 0x7f);
val = val >> 7;
recuento ++;
}
mientras que (cuenta--) {
si (cuenta) {
oid_hex [i] = buf [recuento] | 0x80;
} más {
oid_hex [i] = buf [cuenta];
}
i ++;
}
}
res = (char *) malloc (i);
si (res) {
memcpy (res, oid_hex, i);
* len = i;
}
libre (curstr1);
volver res;
}
Las otras dos funciones permiten que el búfer binario se convierta en un conteo hexadecimal (buffer2hex) y viceversa (hex2buffer).
Estas funciones están aquí:char estático *
buffer2hex (const unsigned char * src, size_t len)
{
int i;
char * dest;
char * res;
dest = (char *) malloc (len * 2 + 1);
res = dest;
si (dest)
{
para (i = 0; i <len; i ++, dest + = 2)
sprintf (dest, "% 02X", src [i]);
}
volver res;
}
vacío estático *
hex2buffer (const char * string, size_t * r_length)
{
const char * s;
unsigned char * buffer;
tamaño_t longitud;
buffer = malloc (strlen (cadena) / 2 + 1);
longitud = 0;
para (s = cadena; * s; s + = 2)
{
if (! hexdigitp (s) ||! hexdigitp (s + 1)) {
fprintf (stderr, "dígitos hexadecimales no válidos en \"% s \ "\ n", cadena);
}
((unsigned char *) buffer) [longitud ++] = xtoi_2 (s);
}
* r_length = longitud;
búfer de retorno;
}
Estas funciones son muy convenientes para la depuración, y lo más probable es que muchas las tengan.
Y ahora volvemos a la solución de la tarea, obteniendo una estructura asn1 de la clave pública. Escribiremos una utilidad que generará y guardará una estructura asn1 de la clave pública en el archivo ASN1_PIBINFO.der.
Esta utilidad se encuentra aquí: #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); }
Para verificar el resultado, utilizaremos las utilidades derdump y pp del paquete NSS.
La primera utilidad nos mostrará la estructura asn1 de la clave pública:
$ 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 $
El segundo mostrará el contenido de la clave:
$ 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 $
Aquellos que lo deseen pueden verificar dos veces, por ejemplo, con la utilidad openssl, preferiblemente con un motor
GOST conectado :
$ /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 $
Como puede ver, la estructura ASN1 resultante se prueba con éxito en todas partes.
El algoritmo y la utilidad propuestos para formar estructuras asn1 no requieren el uso de ningún compilador ASN1 y bibliotecas de extensión (el mismo openssl) y resultó ser muy conveniente de usar. Los recordaremos en el próximo artículo, cuando se cumplan los deseos de
Pas y se presente una utilidad gráfica que no solo “analiza los certificados” y verifica su validez, sino que también genera un par de claves en los tokens PKCS # 11, genera y firma una solicitud de un certificado calificado. Con esta solicitud, puede ir a la CA de forma segura para obtener un certificado. Antes de las preguntas, inmediatamente noto que en el último caso, el token debe estar certificado como un sistema de protección de información criptográfica en el sistema de certificación del FSB de Rusia.