One of the most important questions in today’s world is the question of confidence in received data. For example, user А sends data D to user B via email. How can user B be sure that the received data is the same data that was sent by user A? One possible way of resolving this issue is using a digital signature (DS). The following requirements apply to a DS:
The signature content should depend on the signed message;
The sender’s unique information should be used in a signature;
It should be easy to create a signature;
It should be impossible to falsify a signature computationally;
A signature should be small.
This article considers a DS implementation example for binary file integrity checking in Linux (64-bit ELF). We will use a direct DS when only a sender and a recipient are communicating (without a third party/an arbitrator). We will need a private encryption key and a public key (certificate) for this. The sender creates both keys. User A signs an executable file and passes the certificate to user B with the help of safe delivery means. After this, user A sends a signed file to user B. User B runs the received file; if the binary file is corrupted, user B will receive a message that DS verification has failed. To implement this solution, we will need a program for signing binary files and a code that verifies DSs.
DS implementation example
DS implementation includes the following steps:
MD5 source binary file generation;
The creation of two keys: private and public (certificate).
Binary file signing (ELF):
1 MD5 of the binary file is encrypted with the help of the private key;
3.2 The encrypted MD5 is written to a new .sig section of the binary file;
3.3 The certificate is saved to the ~/.ssh folder.
All this can be implemented with the help of the Linux utilities openssl, objcopy, and md5sum. Below you can find an example of a script sign_elf.sh that signs ELF binary files. (Note: source without line numbers is included at the end of this article.)
Figure 1. The process of signing ELF binary. (Source: Auriga)
Let us explore the details of what this script does.
req — certificate creation request
-nodes — create a private plaintext key
-x509 — output — self-signed certificate
-sha256 — encryption algorithm
-newkey rsa:4096 — create a new certificate and RSA private key, number of bits — 4096
-keyout $PRIVATE_KEY — the path to the file where the private key is written to
-out $CERTIFICATE — the path to the file where the certificate is written to-days 365 — number of days for certificate acknowledgement
-subj $SUBJECT — new certificate subject (should have the format of /type0=value0/type1=value1/type2=…). In our case, this is /C=RU/ST=Nizhni Novgorod/L=Nizhniy Novgorod/O=Auriga/OU=DEV/CN=www.auriga.com, where
С — country
ST — state, region, province
L — location
O — organization
OU — organizational department/unit
CN — basic title/container name
The subject is described in detail in RFC-5280 (https://tools.ietf.org/html/rfc5280). After this command is run, a private key will be generated, ~/.ssh/priv.key and certificate ~/.ssh/pub.crt. The private key will be used to encrypt the data, and the certificate will be used for data decryption. Using one private key, it is possible to generate a number of unique certificates for decrypting data that was encrypted with this private key.
Start of loop for all binary files added to the sign_elf.sh script.
Remove the .sig section from our binary file. This needs to be conducted if the file was already signed with our script and we want to re-sign it.
Create a 512-byte text file and add it to our binary file not loaded on the runtime .sig section only for reading, which contains data from the dummy.txt file.
Calculate MD5 of the binary file (with .sig section) and write the result to a text file, binary_name.md5.
This command encrypts the file with MD5 created by line 42 with a private key. Arguments:
dgst — this option indicates that we want to encrypt (sign) data;
-sha256 — encryption algorithm;
-sign $PRIVATE_KEY — encrypt the file with the help of private key $PRIVATE_KEY;
-out $KEY_DIR/$ELF_BIN_SIGNATURE — encrypted data is saved to file $KEY_DIR/$ELF_BIN_SIGNATURE;
$KEY_DIR/$ELF_BIN_MD5 — text file containing data to be encrypted.
Signed file verification. It can be understood by reference to this line that for DS verification we need encrypted data, a certificate that will help us perform verification and data verification. That is, if
x — encrypted data,
y — certificate,
z — verification data,
f(x,y) = z
Remove the old .sig section and add a new one to file $ELF_BIN (binary_name). As data for the new .sig section, data from the signed file $KEY_DIR/$ELF_BIN_SIGNATURE (~/.ssh/binary_name.sha256) is used.
Let us review our DS verification method on runtime. We will use the libcrypto and libssl libraries for this and the following algorithm:
Retrieve the encrypted MD5 from the binary file;
Calculate the binary file MD5 with the .sig section filled out with nulls;
Verify that the encrypted MD5 equals the calculated MD5 with the help of a certificate.
Figure 2. Check integrity. (Source: Auriga)
The code for main.c is shown below.
The main job will be performed by the сheck_integrity() function, which takes the path to the binary file as an argument and returns 0 if the signature verification is passed and (-1) otherwise. The function uses libcrypto library to verify DS. Let us examine the question of check_integrity() function operating in general:
Create a path to the certificate file. The current implementation expects that the file will be kept in the $HOME/.ssh ($HOME/.ssh/pub.crt) folder.
The OpenSSL_add_all_algorithms() function adds all algorithms to a special internal table that OpenSSL uses when calling different functions. Normally, this function is invoked at the beginning, and before exiting the application, EVP_cleanup() is invoked.
Memory is allocated for the X509 structure, which is necessary for the X509 certificate introduction.
The BIO object is created in association with the certificate file. The BIO object can be viewed as an analogue of the file stream returned by the fopen() function.
Read from the certificate file into the X509 structure.
Retrieve the public key from the certificate. The result is saved in the EVP_PKEY structure pointer.
Create the verification context (the result is moved to the EVP_MD_CTX structure). This structure will be used to verify our certificate.
Initialize the mctx verification context. The third parameter contains the encryption algorithm and the fifth — public key. The second and the fourth parameters are not needed for our verification — set NULL.
Retrieve the encrypted MD5 from the binary file. The operation of this function will be described below in more detail.
Calculate the maximum signature size in bytes.
Calculate MD5 of the binary file. The operation of this function will be described below in more detail.
Convert MD5 returned by the calculate_md5() function into the line returned by the md5sum utility. We perform this conversion because the sign_elf.sh script encrypts MD5, which is output by the md5sum utility, and adds the encrypted MD5 into the .sig section.
Add the obtained MD5 as a line into the verification context.
This function performs the verification of the encrypted MD5 calculated on runtime MD5 (we remember that we have the public key and MD5 calculated on runtime in mctx). The EVP_DigestVerifyFinal() function returns a positive number if the verification is passed.
Now we move to the description of functions that work with the ELF header.
The Get_signature() function parses the header of the 64-bit ELF file (the path to the file is set by the first parameter) and retrieves the encrypted MD5 from the .sig section. The encrypted MD5 pointer is saved in the second parameter. Let us provide more insight into this function.
We obtain the file size in bytes.
Open the binary file for reading.
Map the file to memory equal to the file size. After that, we can work with the file as with the memory.
After this, we will search for our .sig section.
We obtain the ELF header pointer — the header is at the beginning of the file.
We obtain a pointer to the sections table (our .sig section is somewhere among them).
Count the total sections number.
Obtain a pointer to the lines table section header. Here is the ehdr field e_shstrndx — this is the lines table index in the sections table.
Obtain the lines table address from the beginning of the file. Here is the sh_offset field — the section shift from the beginning of the file.
Here the loop starts for all sections (shnum). Each section header has the sh_name field — this is line index in the lines table. To retrieve the section nam,e we need to address the offset symbols table sh_name.
If the section name is .sig , then we obtain its size (the field sh_size of the section header) and offset from the beginning of the file (field sh_offset).
With the section beginning pointer and its size, copy the section content and encrypted_md5 array. Now the array has the encrypted MD5.
The Calculate_md5() function code is much like get_signature(). The Calculate_md5() function does the following:
Allocates memory equal to the signed binary file size;
Copies the mapped binary file to the allocated memory;
Searches for the .sig section;
Fills out the .sig section content with nulls;
Calculates MD5 for the retrieved memory content.
Section .sig is filled out with nulls to calculate MD5 for the binary file before it is signed. Here we need to be reminded of how we signed the binary file in the sign_elf.sh script:
touch dummy.txt truncate –size=512 dummy.txt objcopy –add-section .sig=dummy.txt –set-section-flags .sig=noload,readonly “$ELF_BIN”
Here we created a 512-byte file and filled it out with nulls. After that, we the added .sig section into the binary file that contained the dummy.txt file data.
The DS implementation method described above can be improved: Fill out the .sig section not with nulls but with some known numbers, such as with the first 10 bytes from the .data section. To do this, you will need to slightly modify the calculate_md5() function.
The source implementation code without line numbers is provided below.
This article reviewed one of the variants for DS implementation in Linux. A strong hash function encrypted with a sender’s private key lies at the root of the method we described. This implementation does not require writing a driver and is accomplished by standard Linux utilities. Another variant of DS support for Linux is DigSig (http://disec.sourceforge.net/), but, unfortunately, this project is no longer maintained. For more detailed information about DSs and asymmetric encryption, see the RFC pages: 5280 (https://tools.ietf.org/html/rfc5280), 7091 (https://tools.ietf.org/html/rfc7091), 3447 (https://tools.ietf.org/html/rfc3447), and 5959 (https://tools.ietf.org/html/rfc5959).
Kirill Brazhnikov is a software engineer at Auriga. His experience includes low-level software development (host-target development model) in RTOS LynxOS-178 and system programming in Linux.