Memory corruption in JCRE: An unpatchable HSM may swallow your private key
The key has always been a core target of security protection. Due to the limitation of key slots, most cryptocurrency hardware wallets use MCU chips (such as STM32F205RE) to implement. However, people who have higher security requirements to safeguarding the private keys are often interested in Java cards because:
Java card is basically a smart card with hardware implementations of cryptographic algorithms. Private or symmetric keys cannot be extracted from them. The user can only get the results of cryptographic operations from the Java card.
A Java card that has been initialized with communication parameters but has not yet loaded an application (applet) is user-programmable, and there are some free and open source software projects implemented in the form of Java card applets with various functions.
However, we need to note that the Java card is a highly resource-constrained embedded operating environment, and its hardware characteristics should be highly similar to various programmable microcontrollers:
There is no MMU, and there is a certain amount of NVRAM and “VRAM” (general RAM that loses its contents after power-off), and the two are generally in the same address space, only distinguished by the address.
There is no external storage like a disk, and NVRAM can be directly addressed by the CPU, so there is no need for encoding/decoding operations. The data can be atomically copied to NVRAM for long-term storage.
The lifespan of NVRAM is limited, so frequent rewriting of the data should be avoided as much as possible when designing the program.
As we know above these characteristics of Java card, the most suitable language to work in this environment is actually C (or Lisp/Scheme machine?), which can directly access memory addresses and is like an “architecture-independent assembly language”. However, the language that runs on a Java card is Java, which cannot directly access memory addresses and it requires interpretation and garbage collection, and cannot even determine the memory layout of an object. Therefore, the reliability of a Java card depends on the completeness and correctness of the functionality provided by the JCRE (Java Card Runtime Environment) in the card. However, we should note that the JCRE doesn’t have free and open source implementation. The proprietary JCRE is programmed during manufacturing and even the vendor cannot change it after. Therefore, applet developers can only try to “adapt” to the features of the Java Card Runtime Environment, but the functionality of the JCRE is “incomplete” if you compare it to typical JRE. Let’s see two real-life example about what could possibly go wrong in JCRE!
bitlogik filed an issue in the SmartPGP in Jan 2021, stating that the garbage collection has been poorly implemented in all Java cards produced by JCOP, especially the J3R110. Variables referenced only by the stack cannot be properly free after the function returns, which can lead to “out of memory” errors after repeated signing operations, even if the card is removed and re-inserted. The only solution is to reinstall the applet on the card. bitlogik proposed a programming paradigm for Java cards:
* The main goal is to reduce the heap "dynamic" size to nearly zero.
* Avoid all dynamic memory allocation, only let "small and simple" variables space as byte or short. All classes should be instantiated once at card installation. That means the new Signature performs at each signature, should be instantiated as a PGPKey "static" property, which is then instantiated in Persistent with new PGPKey, instantiated with the SmartPGPApplet install. As the Persistent class name suggest, there were some efforts into this direction of creating objects once, this just need to be pushed further, also in the Transient class. The first points is to remove from dynamic heap the Signature.getInstance and some KeyPairs.
* Performs some memory reuse. When some variables are used in some lines, and then not after, but a similar variable type is then used, this can be grouped as a single variable. This goes dramatically against code source reading comprehension by humans, but this is required in order to minimize dynamic object allocation, so reducing the heap size and tackle this issue.
* On some left dynamic creation, the pointer should be put to null at the end of its uses, even at the end of its scope, in order to help the poor virtually non existent garbage collector.
* Remove all final keyword in the remaining (only byte and short) dynamic memory allocation.
* Try to avoid all new XX() in the code, except the instance variables in class, which are mostly instantiated at applet installation for the master classes.
* Since all this involves memory reuse, and the applet code should not rely on the heap manager garbage collector, that means that some data put in temporary variable must be erased or zero fill. This is important for secret data. For example a signature output doesn't require erasing, but an AES MAC encrypted channel key does.
* Test whether this is fixed, with some infinite loop script software, testing a lot of card functions in a loop over and over. It should not crash the app after hours of run. The script we provide as a PoC to trigger this issue loop 200 times to be sure to trigger it. Actually on our test cards, it happens around 20 signatures.
The signing operations wouldn’t causes “memory consumption” after the maintainers eliminated most of the dynamic allocation but repeatedly importing or generating key pairs into the card can still exhaust the resources of the card. Although
JCSystem.requestObjectDeletion() can be used to trigger garbage collection, but support of JCRE for this functionality is not uniform. However, the lifespan of key pairs is comparatively long. As long as users can compile and install the applet themselves, they can take the opportunity to update the applet when generating or importing new key pairs. The remaining issues will not have a significant impact on users of HSM solution like SmartPGP.
wreps8Owt discovered that when SmartPGP was reset to factory settings with KDF enabled on his own J3R180, the PIN code did not revert to the default initial value, which making it impossible to further configure the reset card in April 2023. The applet must be reinstalled. The cause of problem was found after reporting the issue and repeated testing by wreps8Owt and the project maintainers. When a PIN code longer than a certain length (later found to be 16 bytes) was set, the
OwnerPIN object in wreps8Owt’s J3R180 would be structurally destroyed. Although the overly long PIN could be verified, updating an
OwnerPIN object that contained an overly long PIN would result in undefined behavior, which would lead to memory corruption. Setting KDF will replace all PIN codes with 32-byte hash values generated from the plain text PIN code using KDF as the salt, which exceeds the maximum PIN length that will not destroy the
OwnerPIN object, triggering a bug in the JCRE.
The workaround initially given by the project maintainers was to recreate the
OwnerPIN object when it should be updated, but the problematic card also belongs to the J3R series, Case I suggests that this approach may lead to resource exhaustion. Therefore, the stable workaround is the one proposed by wreps8Owt later, which is to limit the maximum PIN length based on the actual situation and abandon KDF functionality, because KDF functionality will inevitably destroy the stability of the applet running on the card. The smartpgp-cli that fixed the bug will fail if it attempts to set a “too long” PIN for KDF, thus avoiding destruction. Although there may be some minor issues after limiting the maximum PIN length, overall it does not affect the usability of the card.
In fact, it is not unacceptable for the specific implementation of Java cards to set a global upper limit on the length of the PIN code. However, the problem is that the Java Card standard does not specify a static member function for the
OwnerPIN class to return the global upper limit of the PIN length or specify that an exception should be thrown when attempting to create an
OwnerPIN object that exceeds the global length limit. This prevents the applet from automatically avoiding memory corruption at runtime, and can only be discovered through “roll the dice” to find the actual global upper limit of the PIN length and then make a workaround for a specific card.
Neither of the two examples mentioned in this write-up will result in the compromise of private keys, but rather will cause the applet to fall into irrecoverable errors. The private keys in the card could be lost once the issue occurs. Smart cards as HSMs are a more secure implementation than MCU based solution (like almost all hardware wallets does), but there are still certain security risks. Even hardware wallets with EAL 5+ certification have records of being exploited. Therefore, we still need to adhere to a defense-in-depth strategy in terms of system security. On the other hand, transparency matters where the open source is the only way to ensure that the entire operating environment of the HSM can be audited properly.
For Java cards, we hope we could have a free and open-source and updatable JCRE in the future. Or some kind of HSM that is functionally similar to Java cards but can be programmed in C language (S0rry, we do not want Rust since we got modern mitigation and sanitizer for which Rust is too rusty, no?), or even directly implemented using general-purpose computing via trusted computing, runtime protection, attack surfaces reduction and so on.