Implementing cryptography (advanced)

Cryptographic libraries

From a developer’s perspective, cryptography is usually used via a cryptographic library, a collection of algorithms and schemes accessed through an application programming interface (API). Many different cryptographic libraries are available in various programming languages, with C variants and Java being among the most common. Libraries have different licensing restrictions, and many are available under various types of open-source licences.

Some libraries (e.g. OpenSSL or BouncyCastle) offer a wide range of features and can be used in diverse applications. Others are more limited and are designed to support only specific use cases. This partly reflects the preferences of the developers, but also the age of the libraries and the development resources available. For example, both of the above libraries were used in a TAU master’s thesis in 2021, in which the PACE protocol was implemented in a card reader for NFC use: OpenSSL for iOS and BouncyCastle for Android.

Some libraries are better maintained than others. For example, before the Heartbleed vulnerability (2012–2014), OpenSSL had fallen into a somewhat poor state. As a result, Google and OpenBSD independently created their own separate development branches (“forks”) of it. Heartbleed also led to broader awareness of how important OpenSSL is to the entire Internet ecosystem. This was followed by reforms to the OpenSSL project, and today it is in much better condition, with a larger core developer community, better funding, and more active development.

Some cryptographic libraries are developed by professional software engineers with significant experience in avoiding the kinds of issues discussed in this module. Others are not. In many cases, developers work on a voluntary basis; most of OpenSSL’s code development is carried out in this way. A cryptographic library should have a clear process for reporting bugs and security vulnerabilities to maintainers, and the developers should be committed to addressing these issues in a timely manner.

API design for cryptographic libraries

The application programming interface provided by a cryptographic library is critical. A delicate balance must be found between flexibility and security. Flexibility allows developers to use the library in many different ways, making it more useful. Security requires restricting the API so that developers cannot use the library in unsafe ways. This can be illustrated in the case where an API offers symmetric encryption. Should the library allow direct access to the block cipher itself, enabling developers to supply parameters and plaintext blocks directly? Some developers might need this at some stage, but an inexperienced developer might use the block cipher in ECB mode to encrypt large amounts of data—with well-known insecure consequences. This is only a simple example. One might instead consider, for instance, an API that either allows or does not allow the caller to provide the nonce values required for AEAD encryption. An even more demanding case for the user would be allowing them to select parameters for primality testing. This material has not even covered that topic, which is essential, for example, in generating RSA parameters.

Green and Smith [Green2016] present ten principles for cryptographic API design. These are based on case studies and interviews with developers. Later, more formal academic analyses have also been conducted, but these principles are suitable for this course.

Green and Smith note that cryptographic libraries appear to be uniquely prone to misuse and that even small mistakes can lead to major problems. They also observe that as cryptography becomes more widespread, more developers without cryptographic expertise are using these libraries. It is worth adding a local note: later TAU courses significantly enhance expertise precisely in this area within the field of cybersecurity.

The ten principles of Green and Smith can be summarised as follows:

  1. Integrate cryptographic operations into standard APIs; that is, hide cryptography from developers as much as possible.
  2. Ensure APIs are sufficient to meet both security-related and non-security-related requirements. The reasoning is that developers do not primarily think in terms of security goals. Meeting their actual requirements—as observed in interviews—encourages them to use the API instead of implementing their own cryptographic code.
  3. Design the API to be easy to learn without cryptographic expertise.
  4. Do not violate the developer’s mental model of what an API should look like.
  5. Design the API to be usable even without documentation. Developers are unlikely to read it anyway.
  6. Make the API difficult to misuse. Incorrect usage should result in clearly visible errors.
  7. Provide safe and unambiguous default settings.
  8. APIs should have testing modes. Otherwise, developers may disable security features during development to simplify testing and forget to re-enable them. This recommendation has the drawback that code might be released while still in testing mode; ideally, standard software quality assurance should prevent this.
  9. Code using the API should be easy to read and maintain. For example, the number of hash iterations used in deriving a key from a password should not be set via the API but adjusted within the library if necessary. One issue with this recommendation is that internal defaults may be overly conservative and degrade performance in some use cases, illustrating the tension between flexibility and security.
  10. The API should also support interaction with the end user rather than placing the entire burden on the developer. Green and Smith emphasise the importance of error messages: API and library documentation should help developers understand possible failure modes, their security implications, and how calling code should handle resulting errors.

[Green2016] M. Green and M. Smith, “Developers are not the enemy!: The need for usable security APIs,” IEEE Security & Privacy, vol. 14, no. 5, 2016.

. _18-2-3-en:

Challenges of implementing cryptography

The main challenge in implementing cryptography is translating the purely mathematical or pseudocode description of schemes into program code such that, when executed, it still adheres to the security analysis performed at the abstract level. In other words, the challenge is to ensure that the implementation does not introduce mechanisms through which sensitive information can leak and that have not already been anticipated and eliminated during the security analysis. The following presents several ways in which such leakage may occur. These are typically known as side channels.

Attacks involving shared resources

Cryptographic code does not always operate in complete isolation from potential attackers. Modern processors have cache hierarchies in which fast memory is shared among processes, and processes may overwrite each other’s cache contents. For example, in cloud computing environments, processes from different users may run concurrently on the same hardware, even if separated by security mechanisms such as virtualisation. An attacker running in a separate CPU process could deliberately evict specific cache lines and then, after the victim process has executed sensitive code, observe access timings to determine whether the victim used those cache lines. If the victim’s memory access pattern depends on secret data, such as a key, information may leak indirectly. This attack is known as Flush+Reload and was introduced in 2014. Numerous other cache-based attacks are also known. The potential for such attacks was recognised as early as 2002 and was later shown to pose particular risks for AES.

In recent years, researchers have developed a wide range of cache- and microarchitecture-based attacks against cryptographic implementations. These vulnerabilities generally arise because processor designers make architectural trade-offs in pursuit of performance.

Implementation weaknesses

In addition to side channels, cryptographic implementations often suffer from more conventional weaknesses. Keys may not be properly erased after use, or they may inadvertently be included in backups. Decrypted plaintext may be incorrectly passed to the calling application before its integrity has been verified. This can occur in schemes where MAC verification is performed after decryption. Similar issues may arise in streaming applications with limited buffer sizes for decrypted data.

Attacks due to system composition

Systems that combine multiple cryptographic components may inadvertently leak sensitive information due to incorrect integration. Such leaks occur at the system level rather than within individual components. An example is the Zcash cryptocurrency. Zcash provides anonymity for recipients using public-key encryption. A Zcash client determines whether a transaction is intended for it by attempting decryption with its private key. If this fails, no further processing occurs; otherwise, additional components (such as zero-knowledge proofs and commitment verification) are executed. This difference in behaviour may be observable and can break anonymity, even if each individual component is secure.

Attacks by inducing faults

Hardware implementations may also be vulnerable to fault or disturbance attacks, in which an external error is induced at a precisely chosen stage of cryptographic computation. This produces an incorrect result, from which sensitive information—typically a key—can be inferred. The first such attack (1997) targeted an RSA implementation that computed separately with respect to each prime factor (p and q) before forming the result modulo (p·q). A well-timed fault (“glitch”) could cause the computation to yield a result mod p only, leading to successful factorisation. A more recent instance of this type of attack (2014), known as Rowhammer, aims to induce faults in memory locations where keys are stored. This is attempted by repeatedly writing to adjacent memory locations (see Operating systems, including side channels).

Defences

Vulnerabilities in cryptographic implementations are distinct from weaknesses in the underlying algorithms or systems. General techniques for defending against implementation vulnerabilities come from the fields of software and hardware security. These are well summarised in other modules of this material. It is evident that traditional software security is even more important for cryptographic code than for other types of software.

In hardware, commonly used techniques include blinding, masking, threshold techniques, and physical shielding. For software, common approaches include formal specification and verification of software and hardware designs, static and dynamic code analysis, fuzzing, data-flow analysis, the use of domain-specific languages for cryptographic code, and strong typing for modelling and implementing security properties.

Length-based side channels can be mitigated by padding plaintexts to predetermined sizes before encryption and by adding cover or dummy traffic. Secure communication protocols such as TLS and IPsec include features to support this, but they are not widely used. Coding practices have been developed to achieve so-called constant-time cryptography. The core idea is to eliminate, through careful programming, all correlations between sensitive data and observable variables (such as execution time). This requires, among other things, avoiding memory accesses and branching that depend on secret data, as well as certain low-level instructions whose execution time depends on operands. It may also require writing high-level code such that the compiler does not optimise away constant-time properties. Writing constant-time implementations of existing algorithms is not trivial. In some cases, cryptographic designers have addressed this from the outset. For example, Bernstein’s ChaCha20 algorithm was designed with this in mind. Similarly, certain coordinate systems make it easier to achieve constant-time implementations for elliptic curve algorithms.

Generation of random bits

Cryptography relies critically on randomness. Most obviously, random bits are needed for symmetric keys and as input to key generation algorithms for public-key systems. Some signature schemes are also probabilistic, such as RSA-PSS, DSA, and ECDSA.

Cryptographic algorithms—and also protocols—must have access to “strong” sources of random bits. For general applicability, such a source should provide a large number of independent and uniformly distributed bits about which an attacker has no information. Some algorithms may reveal their random values, but general usage requires that they remain hidden.

In an ideal world, every computing device would include a true random bit generator (TRBG, also called TRNG, where N = number), whose output is hidden from attackers. In practice, this has proven difficult to achieve. Intel and AMD processors provide access to processed TRBG output via the RDRAND instruction, but the internal design of these generators is not fully transparent. In the absence of a TRBG, a common approach is for the operating system to collect entropy from weak local sources, such as keystroke timings, disk access times, process identifiers, and packet arrival times. This data is gathered and mixed into an entropy pool. From this pool, pseudorandom bits are extracted as needed to seed a cryptographic pseudorandom number generator (PRNG). NIST has standardised such models (2015). They are used in most operating systems, although often with ad hoc constructions that are difficult to analyse. Research has produced mature formal security models for random bit generators, but this is another case where practice preceded theory.

It is challenging to assess how much actual randomness can be obtained from these weak entropy sources. In some computing environments, such as embedded systems, some or all sources may be absent. This can lead to slow accumulation of entropy after a reboot. A related issue appears in virtual machine environments, where randomness may repeat if it is drawn from the host system too soon after a reset.

There has been long-standing debate on whether random number generators should block their output when the entropy estimate maintained by the operating system falls below a threshold. The short answer is no, provided that the PRNG is cryptographically strong and the entropy pool has been properly initialised after startup. This is because the output of a strong PRNG is computationally indistinguishable from true randomness, even if it is not truly random. Some modern operating systems offer an interface to a generator of the “do-not-block-after-initialisation” type.

Posting submission...