1
$\begingroup$

This question, while related, doesn't answer the security aspect I have in mind. I understand that signatures make it impossible for the tokens to be modified. But if I want the JWS token content to be non-visible, I keep hearing people mention JWE. But is there a security reason why I shouldn't be encrypting them myself?

If I take the JWT, encrypt it with AES-256-CBC (I prefer CBC over GCM because I don't want to fall into equal IV attacks), then MAC it with some hashing function like SHA-512 (HMAC-SHA512), convert both the ciphertext and the MAC to base-64, and put them in a printable string. Am I vulnerable to some security attacks if I use the outcome to authenticate users on a web application? Or is just "not nice"?

Looking at the RFC of JWE, I see there's a similar example. But given I'm not an expert in cryptography, I'm not sure whether I'm missing something.

$\endgroup$
  • $\begingroup$ You can make your own token format and encryption if you sure you know how to do it safely. Generally it is however accept that you should not roll your own encryption protocols. Having said that it is quite strange that JWT requires this kind of double wrapping, it would be more efficient otherwise (there are some other token libraries out there which you can consider instead) $\endgroup$ – eckes Mar 30 at 16:45
  • $\begingroup$ @eckes Rolling my own crypto is out of the question. IIUC, it means that I don't write my own encryption/hashing protocols. I don't think writing my own tokens qualifies to be my own crypto. Btw JWT doesn't require double wrapping... it just isn't made to be encrypted. The data in it is supposed to be visible to everyone, but I'm not super-comfortable with that. I was comfortable with that when I started my project, but now I changed my mind and I'm preferring being a little more paranoid. I'm investigating what changes I have to do to my current system. That's the motivation for the question. $\endgroup$ – The Quantum Physicist Mar 30 at 19:39
  • $\begingroup$ If you want encrypted and signed you would need to wrap JWS inside JWE (the usecase for that admitingly seldom), If you sent data to yourself and can get away with a single shared symmetric key JWE with authenticated encryption would be enough. $\endgroup$ – eckes Mar 30 at 20:27
  • $\begingroup$ @eckes Yeah, basically that's my plan after reading the answer below, except that I don't care about JWE format specifically. I can create my own format. I couldn't find a good and reliable C++ library with JWE. The system is for authenticating users to a webapp. Most likely it's sufficient to use a single symmetric key. $\endgroup$ – The Quantum Physicist Mar 30 at 20:40
  • $\begingroup$ I don’t think there is a big risk in using recent JWE libraries. As long as you only accept a single algorithm for a given key that’s safe. Certainly safer than doing it yourself. $\endgroup$ – eckes Mar 30 at 20:42
3
$\begingroup$

[I]f I want the JWS token content to be non-visible, I keep hearing people mention JWE. But is there a security reason why I shouldn't be encrypting them myself?

On the contrary, there is a security reason why you should avoid JWT altogether: software implementing it tends to be riddled with vulnerabilities which are encouraged by a poor design, where the adversary tells you which algorithm to use in the alg field and the software happily obliges.

The standard examples of exploits are specifying the none algorithm to disable verification altogether, or specifying a secret-key authenticator like HMAC-SHA256 when you meant to use a public-key signature like RSA—the adversary knows the public key, and can therefore compute HMAC-SHA256 under that public key that will pass HMAC-SHA256 verification.

Instead, you are much better off just using a single authenticated cipher on a binary blob of data.

If I take the JWT, encrypt it with AES-256-CBC (I prefer CBC over GCM because I don't want to fall into equal IV attacks), then MAC it with some hashing function like SHA-512 (HMAC-SHA512), convert both the ciphertext and the MAC to base-64, and put them in a printable string. Am I vulnerable to some security attacks if I use the outcome to authenticate users on a web application? Or is just "not nice"?

This just doesn't follow the dogma of JavaScript and JWT cultists. I would say it is not nice for JWT to inflict misbegotten cryptographic disasters on the world in the name of…overengineering or something.

That said, while AES-CBC/HMAC-SHA256 in encrypt-then-MAC composition using randomly chosen IVs is generally OK, you should be careful composing primitives this way, especially ones with sharp edges like AES and CBC:

  • Do not touch anything before you have verified the authenticator (in constant time!). It may be tempting to have one function to decrypt the message, and another function to verify the authenticator, and hope that downstream users will do both—but this is a disaster waiting to happen. Make one function that either verifies and decrypts, or fails altogether without divulging any plaintext. Unauthenticated data is pure evil—don't touch it!

  • Pay close attention to security contracts; note that CBC and GCM are different types of thing. You are evidently aware of the importance of authentication, which is good—I am emphasizing this just because your parenthesis seemed to compare CBC and GCM directly, which is apples-to-oranges because they have totally different security goals; while encrypt-then-MAC with AES-CBC and HMAC-SHA256 is a more similar type of thing to AES-GCM in that they have the same security goals and differ only in usage requirements (and size of authenticator).

  • If you're having a sequential conversation, AES-GCM is may be a better option than AES-CBC/HMAC-SHA256 if it is simpler to use: you can safely use the number of messages you have sent so far as the nonce, and have the receiver require that number to be the nonce. This enables the receiver to quickly drop forgeries on the floor, and which ensures that a large class of insecure implementations will immediately fail to interoperate rather than silently leak secrets.

    (When AES-GCM was introduced to TLS in RFC 5288, they made the mistake of allowing arbitrary sender-chosen nonces; TLS 1.3 in RFC 8446 fixed this mistake so that broken implementations will fail fast and noisily.)

  • If counting is hard because of, say, VM state rollbacks, random IV generation might be hard too for the same reason. If this is relevant, you may want to use a SIV-type scheme, synthetic IV, like AES-SIV, which chooses the IV as a pseudorandom function of (an optional nonce and) the message; at worst, if you repeat a nonce, the adversary can tell whether the whole message was repeated or not.

  • Make sure to generate known-answer test vectors for the whole AES-CBC/HMAC-SHA256 composition that you can automatically do self-tests with.

  • AES invites timing attacks unless you can guarantee all the way down your software stack that you're using hardware support for it like AES-NI. It can be computed in constant time software, but it's very slow to do so. Alternatives like Salsa20 in the the authenticated cipher NaCl crypto_secretbox_xsalsa20poly1305 do not have this flaw. Of course, I don't know how easy it is to use anything other than AES in your software stack; if the tradeoff may be between using AES here and throwing up your hands and giving up on security, maybe AES is OK.


Side note:

I understand that signatures make it impossible for the tokens to be modified.

There are two very different things that JWS conflates under the term ‘signature’, which makes for exceptionally bad API design.

  • Signatures enable anyone who knows a public key to verify a message, but only the owner of the private key can sign messages.

    Signatures are verifiable by a third party, and so are useful as an adversarial audit trail—if you claim to an arbitrator that a vendor stole your money, but they can present the check to the arbitrator, the arbitrator can verify it to determine that you're lying.

    Signatures are relatively expensive to compute, costing hundreds of thousands of CPU cycles per message.

  • Authenticators or message authentication codes (MACs) allow anyone who knows a secret key to verify or authenticate messages.

    Authenticators are not verifiable by third parties—if Bob shows Charlie a message he claims Alice sent him, the putative authenticator from Alice doesn't mean anything because Bob could have forged it too.

    Authenticators like Poly1305 can be some of the cheapest cryptographic primitives ever, costing under one cycle per byte. (HMAC-SHA256 is nowhere near the fastest authenticator but it is still much faster than public-key signature.)

$\endgroup$
  • $\begingroup$ Thank you for the great answer. I learned so much, and will definitely stop using JWT as the first layer. What I learned is that I'll store the data in some blob (which will be my own json data), encrypt it, then MAC it. That's the cheapest + safest token to authenticate users. A digital signature can be used if it's necessary for users to verify the origin, which I have to think about. My app is on Linux, so I'll install libsodium and take a look at these functions and study them. Currently I just use OpenSSL, that's why I have only AES at hand where I created my own high-level functions. $\endgroup$ – The Quantum Physicist Mar 30 at 9:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.