Bytecode verification has been an interesting debate since the very beginning of Java Card. Back then, in 1997, Java was very much about Java applets, and the bytecode verifier was the essential piece of software that allowed untrusted code to run in a browser efficiently (i.e., without doing expensive runtime checks, and without having to setup a complex process of applet vetting). We inherited this view of bytecode verification in Java Card, and the verifier was integrated in the Java Card virtual machine.
Since then, there have been many debates about bytecode verification, and my own position has evolved over the years. Today, my opinion is a bit contrasted, some may even say contradictory. Nevertheless, here it is:
- Bytecode verification has limited use for Java Card runtime security.
- Extended bytecode verification is a very good tool for application vetting.
Let me give a few more details about these two opinions.
Bytecode verification and runtime security
The idea of runtime verification is that it is performed after getting the code, and right before to run it. The code that runs is therefore known correct, at least regarding bytecode execution (unrelated attacks remain possible, of course).
The first big issue with bytecode verification is that, in Java Card, with the split virtual machine model, verification is not performed on the card, but outside of the card, before/during/after converting the application into Java Card’s specific binary format (CAP file). Then, the application is sent to the card over some channel that may be attacked. Here, we rely on GlobalPlatform’s cryptography-based security to protect the integrity of the code. So, basically, we still need to establish trust, and we must say goodbye to the power of running untrusted code from arbitrary developers.
This is not a big issue in practice, because there is no business model that allows arbitrary code to be loaded on cards. However, this causes an organizational problem, as the verification process needs to be well organized. Researchers from Nijmegen and Limoges have shown that it is possible to fool the verification process with fake export files, and then load on a card verified code that will perform type confusion attacks.
Going beyond that, some people have designed applications that can be verified, but have been designed to be attacked through hardware attacks (fault induction, usually). Such attacks, usually known as hybrid attacks, completely fool the bytecode verification process.
Most security experts will then remind you at this point of the discussion that bytecode verification is only one part of the process, that GlobalPlatform is also important, that the origin of code is usually known, that this makes it very difficult to include attack code in an application, etc. By doing this, they are just about accepting the fact that bytecode verification is useless or at best marginally useful: the real security of cards is in the trust between issuers and application providers, and some cryptographic process. Basically, this works because the ecosystems are small enough to be controlled (which is true for cards, and fine for me).
Finally, some vendors claim to have developed defensive virtual machines, which do not require a bytecode verifier, and perform enough runtime verifications to accept any bytecode, good or bad, without any risk. In that case, bytecode verification is really useless, and I believe that it makes things much simpler. Of course, such virtual machines are difficult to design and implement efficiently, but this is definitely possible, and I think that it has been done in the past (or at least that some people has come very close to it).
To conclude, I will get back many years ago. In 2000, Trusted Logic was very proud of its on-card bytecode verifier; the technology worked, but it has not been widely adopted. I believe that the first factor was performance, as verification made the loading and linking process much slower. I also believe that a second factor, less obvious, is that on-card bytecode verification makes the card’s security more brittle, by encouraging VM developers to rely too much on static verification rather than runtime checks (which also prevent other runtime attacks, for instance using fault induction). More than ten years later, most people admit that this was not the way to go, and I just go one step further by stating that bytecode verification is not that useful for runtime security.
Bytecode verification and application vetting
Bytecode verification simply consists of a very simple static analysis of the application code. Basically, this is is a program proof, but a very simple one, whose complexity can be controlled. The objective of such simple proofs is not to demonstrate that a program is correct with respect to its specification, but that this program obeys a predefined set of properties.
For Java bytecode verification, these properties are related to the proper ues of the bytecode instructions. However, the same algorithms can be extended in order to prove many more properties, covering many aspects of application security. Among the things that can be proven, we have the following:
- Method M is not called.
- Method M is not called with arguments A1, …, An.
- Method M is always called before/after method N.
- Method M never throws a
NullPointerException(or any other exception).
- Command INS can only return status codes SW1, …, SWn
- Toolkit buffers are only used when they are available.
- and many more.
Of course, there is no magic. When extending static analysis to many properties, it becomes more and more difficult to avoid false positives, i.e. errors that are caused by weaknesses in the algorithms. Put simply, an application is rejected although it is in fact correct.
Apart from working on the algorithms to make them better, there are two ways to deal with this issue:
- Providing the verification tool to developers, together with explanations about the rules to follow. Usually, it is rather simple for developers to ensure that their application can be verified, provided that they can use the tool throughout the development process.
- Use the tool in a vetting process as a guide for evaluators, who then perform additional verifications. In that case, broken rules are considered as warnings, and then verified by the evaluators. Simple applications can usually be verified easily, and more complex verification requires a few checks, simplified by the fact that the most boring tasks are performed automatically.
I believe in both approaches, but the second one has been proven efficient by my then colleagues of Trusted Labs. By using such a static analysis tool, they are able to optimize the vetting process of Java Card applications (and MIDlets, but this is another story) significantly, by allowing them to focus on the the most important parts of the applications.
In addition, this approach allows us to include many more security rules. For instance, hybrid applications must include code that can be modified into attack code through a simple attack. It is possible to build a library of patterns of such code sequences, and to check for them statically. Then, evaluators can be warned of the possible presence of an hybrid attack and check for it. This can be promising, because such checks (very boring to do, but very unlikely to lead to anything) are usually poorly performed by humans.
Finally, extended bytecode verification, or static analysis, has been proven to be a very efficient optimization tool. All optimizing compilers include a static analysis phase to perform their most complex optimizations. Java Card can take great advantage of this, because of the isolation between applications, and also because of the closed world hypothesis that is sometimes possible, when cards are closed (no additional applications can be downloaded).
So, bytecode verification has many uses in Java Card, but runtime security just isn’t the most compelling.