📜 ⬆️ ⬇️

A simple sprintf-based ASN1 codec

image The ASN.1 transport syntax defines an unambiguous method for converting the values ​​of acceptable types of variables into a sequence of bytes for transmission over a network. In ASN.1, it is called Basic Encoding Rules (BER). The rules are recursive, so that the coding of composite objects is the creation of a chain of coded sequences of constituent objects. The ASN.1 protocol describes the data structure in simple and understandable language .

Each transmitted value - both basic and derived type - consists of three fields:


If you always specify the length of the data field (I consider this a good tone rule), then the end field flag is not used.

There are many different compilers for ASN.1, both paid and free, for different programming languages, but we would like to have something very simple at hand.
')
The overwhelming majority of software developers find the ASN.1 standard difficult . I thought so too until recently. Working in the field of PKI / PKI / cryptography, almost every day you deal with ASN1 structures in the form of X509 certificates, certificate requests, certificate revocation lists. And the list goes on. And so, while working on the utility for creating a certificate request in PKCS # 10 format with generating a key pair on a token / smartcard PKCS # 11, I naturally had to form, in particular, an asn1 public key structure to write it to the certificate request :

C-Sequence C-Sequence (<>) Object Identifier (<>) <oid public key> C-Sequence (<>) Object Identifier (<>) <oid  > Object Identifier (<>) <oid > Bit String (<>) <  > 

Since we used the PKCS # 11 token with the support of Russian cryptography as the ICPP, the source material for this structure was obtained from the token in accordance with the following template:

  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)} } 

Directly from this structure, the values ​​of the CKA_VALUE attribute, which contains the value of the public key, and the values ​​of the attributes CKA_GOSTR3410PARAMS and CKA_GOSTR3411PARAMS, which contain the oid s of the signature parameter and the hash parameter, will be used to fill in asn1-publickeyinfo.

The CKA_KEY_TYPE attribute, which can take on the values ​​CKK_GOSTR3410 and CKK_GOSTR3410_512 (under the conditions when the signature algorithm of GOST R 34.10-2001 continues to operate) ambiguously determines the algorithm of the key pair. If the value of the CKA_KEY_TYPE attribute is CKK_GOSTR3410_512, then, of course, it unambiguously points to the algorithm GOST R 34.10-2012 with a key length of 512 bits (oid = 1.2.643.7.1.1.1.2). But if it is just CKK_GOSTR3410, then ambiguity arises, what type of key does this key belong to: GOST R 34.10-2001 or still it is GOST R 34.10-2012 with a key length of 256 bits. This ambiguity helps to resolve the attribute CKA_GOSTR3411PARAMS.

Immediately, we note that the CKA_GOSTR3410PARAMS and CKA_GOSTR3411PARAMS parameters on the token, in accordance with the recommendations of TC-26, are stored as an object identifier encoded with oid, for example:

\ x06 \ x06 \ x2a \ x85 \ x03 \ x02 \ x02 \ x13, where the zero byte determines the type of sequence (0x06 is the object identifier, see the table below), the second byte indicates the length (in general, the length may take several bytes but about it below) data fields in which oid is stored in a binary type.

If this parameter contains the oid of the GOST R 34.10-2012 hash algorithm with a length of 256 bits (oid = 1.2.643.7.1.1.2.2, in binary form "\ x2a \ x 85 \ x 03 \ x 07 \ x 01 \ x 01 \ x 02 \ x02 "), the key type must be set as GOST R 34.10-2012 with a key length of 256 bits. Otherwise, this is the key GOST R 34.10-2001. The algorithm for determining the type of key may look like this:

 . . . 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; . . . } } . . . 

Now we have all the initial data for creating the asn1- public key structure.

Recall that each element of the asn1-structure consists of three fields:


Here is the coding table for some types of identifiers used in the PKI / PKI:

Type NameShort descriptionType View in DER Encoding
SEQUENCEUsed to describe a data structure consisting of various types.thirty
INTEGERInteger.02
OBJECT IDENTIFIERA sequence of integers.06
UTCTimeTemporary type, contains 2 digits to determine the year17
GeneralizedtimeExtended time type, contains 4 digits for the year.18
SETDescribes the structure of data of different types.31
UTF8StringDescribes string data.0C
NullActually NULL05
BIT STRINGA type for storing a bit sequence.03
OCTET STRINGType to store a sequence of bytes04

When working with asn1-structures, the greatest shock for the uninitiated is the method of encoding the length of the data field, especially during its formation, but if you also take into account the computer architecture (littleendien, bigendien). This is a whole science . And in the process of reasoning about the algorithm for the formation of this field, it came to mind to use the sprintf function, which will take into account the architecture itself, and how the number of bytes for storing the length is determined by the function code, which prepares a buffer with a data type identifier and data length:

 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); } 

The function returns a pointer to a buffer with an asn1 structure, allocated with regard to the data length. It remains to copy this data to the received buffer with an offset by the length of the header. The length of the header is returned via the lenasn parameter.

In order to check how this function works, we will write a simple utility:

 #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); } 

Save it together with the wrap_id_with_length function in the wrap_id_with_length.c file.

Let's broadcast:

 $cc –o wrap_id_with_length wrap_id_with_length.c $ 

The resulting program will run with various source data. The data type is a decimal number.

The resulting program will run with various source data. The data type is a decimal number:

 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$ 

You can check the correctness of the formation of the header using any calculator:



We are all ready to form any ASN1 structure. But before we make small changes to the wrap_id_with_length function and call it

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); } 


As you can see, the changes are minimal. As input parameters, the data itself is added, which inside the function is packaged in an asn1 structure. Moreover, two buffers can be fed to the input at once. It seems to us convenient.

Before presenting a control example, let us give the codes of three more functions. The first function oid2buffer converts oid-s from dotted-decimal form to DER-encoding. We will need this function to convert, in particular, the key pair oid-s (see above).

The function text is here:
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;
}

The remaining two functions allow the binary buffer to be converted to a hex count (buffer2hex) and vice versa (hex2buffer).

These functions are here:
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;
}

These functions are very handy when debugging, and surely many will have them.

And now we come back to the solution of the task, obtaining the asn1-structure of the public key. We will write a utility that will form and save the asn1-structure of the public key in the file ASN1_PIBINFO.der.

This utility is here:
 #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); } 


To check the result, we use the utilities derdump and pp from the NSS package.

The first utility will show us the asn1-structure of the public key:

 $ 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 $ 

The second will show the key content:

 $ 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 $ 

Those interested can double-check, for example, the openssl utility, preferably with a connected GOST- new 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 $ 

As you can see, the resulting ASN1 structure is successfully tested everywhere.

The proposed algorithm and utility for the formation of asn1 structures does not require the use of any ASN1 compilers and extension libraries (of the same openssl) and turned out to be very convenient to use. We will recall them in the next article, when Pas’s wish is fulfilled and a graphical utility is presented that does not only “parse certificates” and check their validity, but also generates a key pair on PKCS # 11 tokens that form and sign a request for a qualified certificate. With this request, you can safely go to the CA for a certificate. Ahead of the questions, I will immediately note that in the latter case, the token must be certified as ICPP in the certification system of the FSB of Russia.

Source: https://habr.com/ru/post/446166/


All Articles