OPAQUE is a cool protocol that combines a Oblivious Pseudo-Random Function (OPRF) and an Authenticated Key-Exchange (AKE) into a protocol where a user holding nothing but a password and a server holding some information protected by the password can establish a shared secret. Because the password is used through an OPRF the server never learns anything about the password or the result of the PRF. A bit longer elaboration on this is written by Matt Green.

Over the last years I have been implementing this protocol. First it was part of libsphinx. Later it was split out into its own libopaque. Later the IRTF CFRG started on a draft specification, which is in constant flux and quite some effort to keep conforming to. This means also that libopaque itself is also unstable as long as there is no final version of the IRTF CFRG specification.

OPAQUE is a strongly recommended replacement for the SRP protocol.

What is it good for?

The shared secret which comes out of the AKE can be used to authenticate the client and the server to each other (the server is authenticated automatically), authenticating the client takes one extra message.

The shared secret can also be used to setup an encrypted channel between the two peers, for example it might be used as a root-key for a double-ratchet from the signal protocol. In this case there is no need for an explicit authentication of the user since the channel itself is implicitly authenticating the user.

Furthermore OPAQUE can be used as a kind of storage mechanism of sensitive data which is stored at the server and can be recovered by a password. For example this can be used to store another key, password or even a cybercoin wallet key.

Another nice feature of OPAQUE is that there is no tokens/messages being sent between the parties that can be replayed by an attacker between the client and the server.

Things OPAQUE doesn't solve

In case a server uses OPAQUE to set up sessions with users, and the user database leaks, an attacker can run offline brute-force attacks against this database to recover any user passwords, and possibly other data protected by this. OPAQUE does make this harder though, the IRTF CFRG draft specifies scrypt as a memory-hard stretching function. Unfortunately the draft does not specify argon2i as such, since there is no I(ER)RTF specification for this. In libopaque argon2i is used though, which should make an attackers life quite more difficult.

Another thing that OPAQUE doesn't solve is phishing attacks in a browser, as soon as someone enters their password into a phishing page in a browser, all is lost. But this is really a problem with browsers which are broken-by-design (in so many ways).

There is a whole lot of other things that OPAQUE doesn't solve. But still it is much better than most other ways to authenticate users with their passwords stored or hashed.

A cool attack

Being one of the first and more mature implementations made it a target for a fabulous attack called Partitioning Oracle Attacks. The attack is based on the fact that some protocols that derive keys from passwords use these keys with algebraic authentication mechanisms, such as Poly1305 or the GCM mode of AES. Due to an algebraic attack it is possible to construct messages that authenticate under a number of password candidates, and thus with just one message and the response to it is possible to test whether one out of many passwords actually is a valid one. And thus instead of sending one message per guessed password, an attacker can send one message and say test 5000 passwords. As far as I understood each tested password increases the message size.

Regarding that paper, there is one quote which needs correction though, the authors write:

By default, libsphinx accepts a Cāˆ— only up to length 4 MB due to a memory management bug ā€” it crashes for larger ciphertexts due to a statically allocated buffer. Once fixed, it accepts cipher-texts of up to 2 GB. This would enable splitting ciphertexts with degree up to k = 1.25Ɨ108.

As the saying goes: "this is not a bug, this is a feature", an intentional feature. libopaque implements everything with variables allocated on the stack, there is no usage of the heap. Normally your Unix limits the size of the stack, you can check this by running

ulimit -s

Which in my case reports 8MB, and if something writes more than space is available on the stack, then the application crashes, it's an out of memory condition and guard pages raise a segfault. The authors wrote: "once fixed", meaning they allocated a huge 2GB buffer using malloc() on the heap (running ulimit -s 2147483648 would've also done it). This is kind of limitation applies to all languages, this even happens the same way in a rust implementation.

Anyway, even before publication of that awesome paper I added more defense-in-depth measures like also maximizing the size of the "C*" buffer to only a couple kilobytes. So libsphinx despite being theoretically vulnerable had a bunch of defensive-coding measures deployed that made the original code quite resistant to this attack. The root-cause of the attack has been fixed independently before the publication of the attack paper.


libopaque comes with a lot of native bindings: erlang, go, java, js, lua, php7, python and ruby are part of the core library, and my dear friend dnet maintains a rust binding opaqueoxide. Also if you like to do things in shell scripting - using socat or similar tools - there is standalone binaries for each step of the protocol.


I'm grateful to creemama who supplied the initial implementation of the js bindings.

This project was funded through the NGI0 PET Fund, a fund established by NLnet with financial support from the European Commission's Next Generation Internet programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310.


next posts >
< prev post

Proudly powered by Utterson