📜 ⬆️ ⬇️

Secure crypto programming. Part 2, final

We are continuing to translate the set of secure crypto-programming rules from Jean-Phillip Omasson ...

Prevent compiler tampering with parts of the code that critically affect security.


Problem


Some compilers optimize operations that they deem useless.

For example, the MS Visual C ++ compiler considered the | memset | in the following snippet of the Tor anonymous network implementation:

int crypto_pk_private_sign_digest(...) { char digest[DIGEST_LEN]; (...) memset(digest, 0, sizeof(digest)); return r; } 

However, the role of this operator | memset | is to clear the buffer | digest | from confidential data so that any subsequent readings of data from the uninitialized stack will not allow you to receive confidential information.
')
Some compilers believe that they can remove conditional checks, considering the code to be erroneous anywhere in the program. For example, finding the following code snippet

  call_fn(ptr); //   ptr. //    if (ptr == NULL) { error("ptr must not be NULL"); } 

some compilers will decide that the condition | ptr == NULL | should always be FALSE, because otherwise it would be incorrect to dereference it in the function | call_fn () |.

Decision


Analyze the compiled code and make sure that all instructions are present in it. (This is not possible for standard-sized applications, but this should be done for a security-critical code snippet).

Understand what optimizations your compiler can do and carefully evaluate the effect of each of them in terms of the principles of safe programming. In particular, be careful with optimizations that remove code snippets or branching, as well as code snippets that prevent errors that “cannot occur” if the rest of the program is correct.

Whenever possible, consider disabling that optimization during compilation, which removes or weakens the verification of conditions affecting security.

To prevent deletion of instructions through optimization, the function can be redefined using the volatile keyword. This is for example used in libottery when overriding | memset |:

 void * (*volatile memset_volatile)(void *, int, size_t) = memset; 

In C11, a call to memset_s is introduced, for which deletion during optimization is prohibited.

 #define __STDC_WANT_LIB_EXT1__ 1 #include <string.h> ... memset_s(secret, sizeof(secret), 0, sizeof(secret)); 


Do not mix safe and insecure software interfaces.


Problem


Many programming environments provide different implementations of the same software interfaces — their functionality is outwardly the same, but the security properties are radically different.

This problem is typical of random number sensors: OpenSSL has | RAND_bytes () | and | RAND_pseudo_bytes () |, the BSD C-libraries have | RAND_bytes () | and | RAND_pseudo_bytes () |, in Java - | SecureRandom | and | Random |

Another example would be the fact that in systems that provide time-independent byte word comparison functions, at the same time, there are variations that can leak over time.

Bad decisions


Sometimes the function is safe on some platforms and dangerous on others. In these cases, programmers use this function, considering that their code will be executed on platforms where it is safe. This is a bad approach, because the code can be ported to other platforms and become insecure - no one will notice.

In systems that allow redefinition of platform-specific functions, some programmers override insecure functions with safe functions and write programs using a software interface, which is normally unsafe. This is a rather controversial approach, since it causes the programmer to write code that looks unsafe. Moreover, if the overridden method does not work ever, the program will become unsafe and this cannot be determined. And finally, this will lead to the fact that the code fragments of such programs will be unsafe, if they are copied to other projects.

Decision


If possible, do not use unsafe security options. For example, a PDCH based on a strong stream cipher with a random initial fill is fast enough for most applications. Independent of the data type, a memcmp replacement is also fast enough to be used for all the memory comparison operations.

If you cannot remove unsafe functions, redefine them so that an error is generated at the compilation stage, or use static code analysis tools to detect and warn about the use of unsafe functions. If you can redefine an insecure function with its safe option, then for greater security, never call the insecure API and make sure that you can detect the fact of its use.

If you need to leave both options (safe and unsafe) make sure that the names of the functions are so different that it will be difficult to accidentally use the unsafe option. For example, if you have a secure and insecure PDCP, do not call the unsafe variant “Random”, “FastRandom”, “MersenneTwister” or “LCGRand” - instead name it, for example, “InsecureRandom”. Design your programming interfaces so that using insecure functions is always a little scary.

If your platform provides an unsafe version of a function without a name that says it is insecure and you cannot remove this function, use a system call wrapper with a safe name, then by static code analysis, identify all uses of the unsafe name.

If the function is safe on some platforms and unsafe on others, do not use the function directly: define and use a secure wrapper instead.

Avoid confusing security levels and cryptographic primitive abstractions at the same API level.


Problem


When it is not clear what analysis different parts of the program interface require, the programmer can easily make a mistake in what functionality they can safely use.

Consider the following example (invented, but similar to those found in real life) of the RSA software interface:

 enum rsa_padding_t { no_padding, pkcs1v15_padding, oaep_sha1_padding, pss_padding }; int do_rsa(struct rsa_key *key, int encrypt, int public, enum rsa_padding_t padding_type, uint8_t *input, uint8_t *output); 

Suppose that the “key” parameter contains the components of the details, then the function can be called in 16 ways, many of which are meaningless and some are unsafe.
encryption / decryptionsymmetrical / asymmetrical
padding type
remarks
00noneDecryption without padding. The possibility of forgery.
00pkcs1v15Decryption PKCS1 v1.5. Possibly subject to Blainebacher's attack.
00oaepOAEP decryption. A good option.
00pssPSS decryption. A rather strange variant, possibly leading to unintended errors.
0onenoneSigned without padding. The possibility of forgery.
0onepkcs1v15Signature PKCS1 v1.5. Suitable for some applications, but it is better to use the PSS signature.
0oneoaepOAEP Signature. Suitable for some applications, but it is better to use the PSS signature.
0onepssSigned PSS. Very good option.
.........the remaining options (encryption and signature verification).


Note that only 4 of the 16 possible ways to call this function are safe, 6 more are unsafe, and the remaining 6 in some cases can cause problems with the application. This API is only suitable for developers who understand the implications of using various add-ons in the RSA system.

Now imagine that we add software interfaces for block encryption in various modes, key generation, various message authentication codes and signatures. Any programmer who tries to develop the correct function that implements data authentication and encryption using such software interfaces will have a huge number of choices, while the number of secure options will obviously decrease.

Decision




Use unsigned types to represent binary data.


Problem


In some C-like languages, signed and unsigned integer types are different. In particular, in C the question is whether the type | char | landmark depends on implementation. This can lead to a problem code, such as, for example, the following:

 int decrypt_data(const char *key, char *bytes, size_t len); void fn(...) { //... char *name; char buf[257]; decrypt_data(key, buf, 257); int name_len = buf[0]; name = malloc(name_len + 1); memcpy(name, buf+1, name_len); name[name_len] = 0; //... } 

If | char | unsigned, this code behaves as we expect it to. But if | char | signed | buf [0] | can take negative values, resulting in very large values ​​of the arguments of the | malloc | functions and | memcpy | and heap damage potential if we try to set the value of the last character to 0. The situation can be even worse if | buf [0] | equal to 255, then name_len will be equal to -1. Thus, we allocate a buffer of size 0 bytes in memory, and then we copy | | (size_t) -1 memcpy | into this buffer, causing the heap to clog up.

Decision


In languages ​​that distinguish between signed and unsigned byte types, implementations must use unsigned types to represent byte strings in their APIs.

Clear the memory of secret data


Problem


In most operating systems, the memory used by one process can be used by another process without prior cleaning, because the first process is stopped or the memory is returned to the system. If the memory contains secret keys, they will be available to another process, which increases the chance of their compromise. In multi-user systems, this makes it possible to determine the keys of other users of the system. Even within the same system, this situation may lead to the fact that previously relatively “safe” vulnerabilities could lead to leakage of secret data.

Decision


Clear all variables that contain sensitive data until you forget about them and use them. Using the function | mmap () | remember that running | munmap () | instantly frees up memory and you lose control of it.

To clear memory or destroy objects that go out of your sight, use platform-specific memory cleaning functions, where possible - such as | SecureZeroMemory () | for win32 or | OPENSSL_cleanse () | for openssl.

A more or less universal solution for C could be:

 void burn( void *v, size_t n ) { volatile unsigned char *p = ( volatile unsigned char * )v; while( n-- ) *p++ = 0; } 


Use "strong" randomness



Problem


Many cryptographic systems require sources of randomness, and such systems may become insecure even in the case of small deviations from randomness in such sources. For example, the leakage of even one random number in a DSA will lead to an extremely fast identification of the secret key. Inadequate randomness is quite difficult to determine: the error of the Debian random number generator in OpenSSL went unnoticed for two years, leading to the compromise of a large number of keys. The requirements for random numbers for cryptographic applications are very strict: many pseudo-random number generators do not satisfy them.

Bad decisions


For cryptographic applications



Decision


Minimize the use of randomness by selecting primitives and their design (for example, Ed25519 allows you to get curves for electronic signatures in a deterministic way). For generating random numbers, use sources provided by the operating systems and guaranteed to meet cryptographic requirements, such as | / dev / random |. On platforms with limited resources, consider using analog sources of random noise and a good mixing procedure.

Be sure to check the values ​​produced by your sensor to make sure that the bytes received are as they should be and that they were recorded properly.

Follow the recommendations of Nadi Heninger et al. In section 7 of their article .

On Intel processors with the Ivy Bridge architecture (and later generations), the built-in generator guarantees high entropy and speed.

On Unix systems, | / dev / random | or | / dev / urandom |. However, the first one has the blocking property, i.e. it does not return values ​​if it believes that not enough randomness has been accumulated. This property limits convenience.
its use, and therefore | / dev / urandom | used more often. Use | / dev / urandom | simple enough:

 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> int main() { int randint; int bytes_read; int fd = open("/dev/urandom", O_RDONLY); if (fd != -1) { bytes_read = read(fd, &randint, sizeof(randint)); if (bytes_read != sizeof(randint)) { fprintf(stderr, "read() failed (%d bytes read)\n", bytes_read); return -1; } } else { fprintf(stderr, "open() failed\n"); return -2; } printf("%08x\n", randint); /* assumes sizeof(int) <= 4 */ close(fd); return 0; } 

However, this simple program may not be sufficient for the safe generation of randomness: it is safer to perform additional error checks as in the function | getentropy_urandom | LibreSSL

 static int getentropy_urandom(void *buf, size_t len) { struct stat st; size_t i; int fd, cnt, flags; int save_errno = errno; start: flags = O_RDONLY; #ifdef O_NOFOLLOW flags |= O_NOFOLLOW; #endif #ifdef O_CLOEXEC flags |= O_CLOEXEC; #endif fd = open("/dev/urandom", flags, 0); if (fd == -1) { if (errno == EINTR) goto start; goto nodevrandom; } #ifndef O_CLOEXEC fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC); #endif /* Lightly verify that the device node looks sane */ if (fstat(fd, &st) == -1 || !S_ISCHR(st.st_mode)) { close(fd); goto nodevrandom; } if (ioctl(fd, RNDGETENTCNT, &cnt) == -1) { close(fd); goto nodevrandom; } for (i = 0; i < len; ) { size_t wanted = len - i; ssize_t ret = read(fd, (char *)buf + i, wanted); if (ret == -1) { if (errno == EAGAIN || errno == EINTR) continue; close(fd); goto nodevrandom; } i += ret; } close(fd); if (gotdata(buf, len) == 0) { errno = save_errno; return 0; /* satisfied */ } nodevrandom: errno = EIO; return -1; } 

On Windows systems | CryptGenRandom | from the Win32 API produces pseudo-random bits suitable for use in cryptography. Microsoft offers the following use case:

 #include <stddef.h> #include <stdint.h> #include <windows.h> #pragma comment(lib, "advapi32.lib") int randombytes(unsigned char *out, size_t outlen) { static HCRYPTPROV handle = 0; /* only freed when program ends */ if(!handle) { if(!CryptAcquireContext(&handle, 0, 0, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_SILENT)) { return -1; } } while(outlen > 0) { const DWORD len = outlen > 1048576UL ? 1048576UL : outlen; if(!CryptGenRandom(handle, len, out)) { return -2; } out += len; outlen -= len; } return 0; } 

If you focus on use in Windows XP or later versions, the above code on CryptoAPI can be replaced by | RtlGenRandom |

 #include <stdint.h> #include <stdio.h> #include <Windows.h> #define RtlGenRandom SystemFunction036 #if defined(__cplusplus) extern "C" #endif BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength); #pragma comment(lib, "advapi32.lib") int main() { uint8_t buffer[32] = { 0 }; if (FALSE == RtlGenRandom(buffer, sizeof buffer)) return -1; for (size_t i = 0; i < sizeof buffer; ++i) printf("%02X ", buffer[i]); printf("\n"); return 0; } 

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


All Articles