We tried backslashing all the functions
When applications need to transfer cryptographic identifiers into other systems, for example within JSON, they often need to encode them into a safe format. This can however leak information about the secret, unless the encoding is done with a “constant time” algorithm, such as implemented by the paragonie/constant_time_encoding library.
Constant time algorithms are generally slower than the equivalent non-constant time algorithm due to the additional work that needs to be done. Nevertheless we should strive to make them as fast as possible as long as security is maintained.
This blog post discusses the pull request that makes the encoding algorithm quite a bit faster by relying on a simple PHP OPcache optimization.
Pure PHP vs Native Libsodium Implementation
The library is implemented in pure PHP for maximum compatibility across a wide range of web hosting services and PHP versions, which might not necessarily have all optional PHP extensions available. In version 3.1.0, the maintainers, however added optional support for the “sodium” extension, which includes constant time implementations for common encodings. This optional support is implemented by checking for the availability of the sodium extension whenever the encoding function is called and then either using the functions from sodium or the pure PHP implementation.
For a simple test script:
This provided for an impressive 14× improvement in performance with libsodium vs the pure PHP variant:
Adding Backslashes to All Function Calls
An astute observer might have noticed that some of the functions in the above snippet from the library have a leading backslash, whereas some do not. Adding the backslash is commonly done in security-sensitive code to make sure that a malicious library is not able to “sneak in” fake implementations into the namespace of the security-sensitive library. We should therefore add a leading backslash to the extension_loaded() and sodium_bin2hex() calls as well to make sure we are actually using the Sodium extension and nothing else. We will take a closer look at the following change which is an excerpt from our PR #64.
It turns out this also further improves performance to 18× the baseline (1.3× the original sodium implementation):
How the code is optimized by PHP
There are two reasons why this code is much faster:
- Without the backslash, PHP will first need to check whether there’s a
sodium_bin2hex()in the current namespace before falling back to the global function. This fallback is cached per call and thus only happens once, though. - We wrote about “compiler optimized functions” before, which OPcache evaluates at compile-time.
\extension_loaded()is among these functions.
This means that OPcache will replace the extension_loaded() check by just true :
It will then notice that the if() is useless and remove it:
And then the entire code after the try-catch becomes unreachable, since either a return statement will be hit or an Exception will be thrown:
In the end, the entire pure-PHP implementation is gone and the check whether or not to use it is as well. Instead the Hex::encode() method has become a thin wrapper around \sodium_bin2hex(). This is also visible in the resulting OPcodes.
Before:
After:
After all the optimizations—both the Sodium support added by the maintainer and the backslashes added by the featured PR #64—, the hexadecimal encoder now only has a roughly 2.1× overhead over the non-constant-time bin2hex() when the Sodium extension is installed. This makes it feasible to err on the side of caution and use the constant time encoder as the default choice, falling back to the insecure alternatives only when best performance is required for a confirmed non-security-sensitive use case.
The same optimization is applicable not just to functions, but also to constants. If the constant is provided by an extension and the name is fully-qualified, this allows OPcache to insert the constant’s value at compile time, which then might allow to fully evaluate individual expressions. A PHP version-compatibility check using if (\PHP_VERSION_ID < 80500) { /* … */ } would be an example. The compatibility code within the if() will only exist for PHP versions below PHP 8.5, and the if() will be fully removed at runtime when upgrading to PHP 8.5.
Time to get started with Profiling! With our free trial.
Does importing global symbols make PHP code faster?