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 / decryption | symmetrical / asymmetrical
| padding type
| remarks |
---|
0 | 0 | none | Decryption without padding. The possibility of forgery. |
0 | 0 | pkcs1v15 | Decryption PKCS1 v1.5. Possibly subject to Blainebacher's attack. |
0 | 0 | oaep | OAEP decryption. A good option. |
0 | 0 | pss | PSS decryption. A rather strange variant, possibly leading to unintended errors. |
0 | one | none | Signed without padding. The possibility of forgery. |
0 | one | pkcs1v15 | Signature PKCS1 v1.5. Suitable for some applications, but it is better to use the PSS signature. |
0 | one | oaep | OAEP Signature. Suitable for some applications, but it is better to use the PSS signature. |
0 | one | pss | Signed 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
- Provide high-level software interfaces. For example, provide functions that implement data encryption and authentication, which use only strong algorithms and in a safe manner. When you write a function that provides various combinations of symmetric and asymmetric algorithms and their modes of operation, make sure that this function does not allow the use of unsafe algorithms and their unsafe combinations.
- When possible, avoid low-level APIs. Most users do not need to use RSA without add-ons, use a block cipher in ECB mode, or use a DSA signature with a random value selected by the user. These functions can be used as building blocks in order to implement something strong — for example, do OAEP padding before an RSA call without an add-on, use ECB encryption for blocks 1,2,3, ... to implement counter mode or use a random or unpredictable byte sequence for a random DSA value, but practice shows that they will often be used incorrectly rather than correctly.
Some other primitives are necessary for the implementation of certain protocols, but most likely will not be suitable for the implementation of new protocols. For example, you cannot implement a TLS browser without CBC, PKCS1 v1.5 and RC4, but any of these primitives is not a good option.
If you provide a cryptographic module for use by inexperienced programmers, it is better to avoid such functions completely and select (for the API) only functions that implement well-described high-level secure operations.
- If you still have to provide the interface to both experienced and inexperienced users, clearly separate the high-level and low-level software interfaces. The “secure encryption” function should not be the same function as “incorrect encryption” with slightly modified arguments. In languages ​​that separate functions and types into packages and headers, safe and insecure crypto functions should not be contained in the same packages and headers. In languages ​​with subtypes, there must be separate types for secure crypto implementations.
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(...) {
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
- Do not rely on predictable sources of randomness, such as time stamps, identifiers, temperature sensors, etc.
- do not rely on pseudo-random common function functions such as | rand () |, | srand () |, | random () | libraries | stdlib | or | random | python language
- Do not use the Mersenne Twister Generator (Mersenne Twister)
- Do not use resources like www.random.org (random data may be known to third parties or may also be used by them).
- Do not use your own random number generator, even if it is based on strong crypto-primitive (unless you know exactly what you are doing).
- Do not use the same random bits in different places of the application, for their "economical" spending.
- Do not conclude that the generator is only resistant because it passes Diehard or NIST tests .
- Do not conclude that a cryptographically stable generator necessarily protects you from reading ahead and reading backwards.
- Never use “randomness” in its pure form as random data (analog random sources often have deviations, so the N bits received from such a source have less than N random bits).
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); 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 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; } 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; 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; }