DEV Community

Scott Brady
Scott Brady

Posted on • Originally published at scottbrady91.com

JWTs: Which Signing Algorithm Should I Use?

JSON Web Tokens (JWTs) can be signed using many different algorithms: RS256, PS512, ES384, HS1; you can see why some developers scratch their heads when asked which one they would like to use.

In my experience, many of the mainstream identity providers have historically only offered RS256 or at least defaulted to it. However, thanks to initiatives such as Open Banking, these identity providers are now expanding their support to cover more signing algorithms, which means you will need to start understanding which ones to use.

I am not a cryptographer, but through my work with OpenID Connect and FIDO2, I have gained a practitioner’s understanding of the various signing algorithms and the cryptography communities’ general feelings towards each one. In this article, I’m going to arm you with some of that knowledge so that you can understand what each “alg” value means and choose the best signing algorithm available to you.

TL;DR: EdDSA > ECDSA or RSASSA-PSS > RSASSA-PKCS1-v1_5 and know what to expect.

Algorithm (alg) Values

Before we look at each family of signature algorithms, let’s first clarify what we mean by “alg” values such as RS256. These are JSON Web Algorithms (JWA), which are part of the JavaScript Object Signing and Encryption (JOSE) family. You’ll see “alg” values in JWT headers, telling you how the JWT was signed, and in JSON Web Keys (JWK), telling you what algorithm a key is used for.

As a general rule of thumb, an “alg” value, such as RS256, can be broken down as: RS (signature algorithm) 256 (hashing algorithm).

  • Signature algorithm family: In this case, RS means RSASSA-PKCS1-v1_5.
  • Hashing algorithm used by the signature algorithm. In this case, 256 means SHA-256.

Most signing algorithms have variants for SHA-256, SHA-384, and SHA-512. In some cases, you can even have something like “RS1”, which uses SHA-1 🤢 and is required for FIDO2 conformance.

These algorithms are typically defined in RFC 7518, but you can find a full list of supported algorithms in the JOSE IANA registry.

Which Hashing Algorithm Should I Use?

SHA-256, SHA-384, and SHA-512 are all variations of the same hashing algorithm family: SHA-2.

As a rule of thumb, the number in an algorithm refers to the size of the hash it will generate. For example, SHA-256 will produce a 256-bit hash, while SHA-512 will produce a 512-bit hash.

The level of security each one gives you is 50% of their output size, so SHA-256 will provide you with 128-bits of security, and SHA-512 will provide you with 256-bits of security. This means that an attacker will have to generate 2^128 hashes before they start finding collisions, thanks to the birthday bound. This is why we use a minimum of 128-bit security.

You’re not going to be needing anything better than SHA-256 any time soon. Just don’t use SHA-1.

Validation: Know your Algorithm

Every application validating JWT signatures should know ahead of time which algorithms to expect and exactly which key to use. You can do this by assigning each public key to an algorithm (e.g. this key is for RS384, this one for ES256). When you have many keys for a single algorithm, you can use the key ID (kid) in a JWT header to understand which one to use.

Basically, you want to make sure the kid and alg values in a JWT match what you expect. If they do not match, then someone is up to no good.

{
    "typ": "JWT",
    "kid": "123", // is this key...
    "alg": "RS256" // ...allowed to be used for this algorithm?
}
Enter fullscreen mode Exit fullscreen mode

Protocols such as OpenID Connect facilitate this using a discovery document and a JSON Web Key Set (JWKS) available on an endpoint protected by TLS.

These days, you should not trust the “alg” value in the JWT header alone, nor should you accept JWTs with an algorithm of “none” or JWTs with a public key embedded in its header.

RSASSA-PKCS1-v1_5 (e.g. RS256)

RS256 = RSASSA-PKCS1-v1_5 using SHA-256

While RSAES-PKCS1-v1_5 is no longer safe for encryption, RSASSA-PKCS1-v1_5 is still suitable for digital signatures. As I mentioned earlier, in my experience, RS256 has historically been the default for most JWT implementations, with many SaaS identity providers only offering this signature algorithm. It’s hard to find a system that can’t support JWTs signed with RS256.

JWTs signed with RSASSA-PKCS1-v1_5 have a deterministic signature, meaning that the same JWT header & payload will always generate the same signature.

RSASSA-PKCS1-v1_5 has been around for a long time, but these days, you should generally prefer RSASSA-PSS (RSA with a probabilistic signature). That’s not to say RSASSA-PKCS1-v1_5 is broken but rather that RSASSA-PSS simply has desirable features that the other does not. In fact, RFC 8017 now considers RSASSA-PSS a requirement when using RSA for signing:

Although no attacks are known against RSASSA-PKCS1-v1_5, in the interest of increased robustness, RSASSA-PSS is REQUIRED in new applications. - RFC 8017

That being said, when discussing Bleichenbacher’s attacks against the RSA PKCS#1 encryption and signature standards, David Wong in Real-Word Cryptography shares an interesting statistic:

Unlike the first attack that broke the encryption algorithm completely, the second attack is an implementation attack [against signature validation]. This means that if the signature scheme is implemented correctly (according to the specification), the attack does not work.

Yet, it was shown in 2019 that many open source implementations of RSA PKCS#1 v1.5 for signatures actually fell for that trap and mis-implemented the standard, which enabled different variants of Bleichenbacher’s forgery attack to work! - Real-World Cryptography

Since the attacks are against signature validation, you will have to be confident that all recipients who validate your JWTs are using a library that is not vulnerable to Bleichenbacher’s attack. That would be difficult if you are dealing with many 3rd parties.

The work around Open Banking, such as OpenID’s Financial-grade API (FAPI), does not allow the use of RSASSA-PKCS1-v1_5. For my regular readers, this was the only algorithm available in IdentityServer up until IdentityServer4 version 3.

Further Reading

RSASSA-PSS (e.g. PS256)

PS256 = RSASSA-PSS using SHA-256 with MGF1 with SHA-256

RSASSA-PSS is the probabilistic version of RSA, where the same JWT header and payload will generate a different signature each time. Unlike other algorithms, this is probabilistic in a good way; while a random value may be used during signature generation, it is not critical to security. In general, it’s a lot simpler to implement and therefore harder to get wrong.

If you want to use an RSA key, then it’s recommended that you use RSASSA-PSS over RSASSA-PKCS1-v1_5, but luckily an RSA key can be used for either signature scheme. Signature length is also identical between the two.

The UK’s Open Banking initially mandated the use of PS256, but later opened it up to ES256.

Further Reading

ECDSA (e.g. ES256)

ES256 = ECDSA using P-256 and SHA-256

In the case of Elliptical Curve Digital Signing Algorithms (ECDSA), the number in ES256 that refers to the hashing algorithm also relates to the curve. ES256 uses P-256 (secp256r1, aka prime256v1), ES384 uses P-384 (secp384r1), and, the odd one out, ES512 uses P-521 (secp521r1). Yes, 521. Yes, even Microsoft has typoed this.

Elliptical Curve Cryptography (ECC) is much harder to crack than RSA (or maybe we are just really good at breaking RSA). As a result, ECDSA can use much shorter keys than RSA along with much shorter signatures. A short Elliptical Curve (EC) key of around 256 bits provides the same security as a 3072 bit RSA key.

You will often see ECDSA listed as faster than its equivalent in RSA, but this is only really true for signature generation; signature validation is still typically faster with RSA. With JWTs, you’re most likely going to be signing once and verifying many times.

JWTs signed with ECDSA have a probabilistic signature, meaning that the same JWT header & payload will always generate a different signature. But unfortunately, ECDSA is probabilistic in a bad way, where random generation is vital to the security of the signature.

ECDSA uses a random nonce (no more than once) that is generated per signature. Failure to only ever use a nonce value once makes the private key easily recoverable, and this has been seen in the wild with both Sony’s Playstation 3 and Bitcoin. With the Playstation 3, the private key was recovered due to a static nonce, and with Bitcoin, Android users were affected due to a bug in Java’s SecureRandom class on Android. If random values are required for the security of a probabilistic signature, then you should prefer a deterministic signature that does not.

Whereas RSASSA-PKCS1-v1_5 has issues with signature validation, ECDSA has issues with signature generation, which is much easier to deal with when you are the token issuer.

ECDSA is gaining popularity but seems to generally be frowned upon by cryptographers due to how elliptical curve cryptography was implemented, with concerns around implementation difficulty due to the use of random values. It has a better reputation than RSA, but cryptographers are still advocating the migration to EdDSA.

The curves JOSE initially used where defined by NIST. If you are concerned about using curves defined by NIST but want to use ECDSA, a popular alternative is to use a Koblitz curve, such as secp256k1 (as opposed to secp256r1). Kobiltz curves are a few bits weaker, but if you have concerns that the unexplained random numbers used in the NIST curves indicate another NSA backdoor, then Kobiltz curves offer an increasingly popular alternative. You can find usages on these curves in Bitcoin, Ethereum, and FIDO2. However, you should be using EdDSA if you want to use a non-NIST curve. In JOSE, algorithms using Kobiltz end with a K, e.g. ES256K.

Further Reading

EdDSA

EdDSA = an EdDSA signature algorithm was used 🤷‍♂️

EdDSA bucks the trend of the previous algorithms and uses a single alg value. Instead, it relies upon the curve (crv) defined in a pre-agreed key.

For example, a JWK containing an EdDSA public key would look like the following:

{
  "kty": "OKP",
  "alg": "EdDSA",
  "crv": "Ed25519",
  "x": "60mR98SQlHUSeLeIu7TeJBTLRG10qlcDLU4AJjQdqMQ"
}
Enter fullscreen mode Exit fullscreen mode

This forces the modern behavior of using the curve assigned to the key, as opposed to from the JWT, and eliminates various alg related attacks.

EdDSA is a form of elliptical curve cryptography that takes advantage of twisted Edwards curves. It is a variant of Schnorr’s signature system (rather than DSA). EdDSA is fast at both signing and validation, has a short signature, and side-steps whole classes of security vulnerabilities.

RFC 8037 defines JOSE support for the following EdDSA variants:

  • Ed25519: a 255-bit curve Curve25519 (32-byte private key, 32-byte public key, 64-byte signature). Signing uses SHA-512. Provides 128-bit security
  • Ed448: a 448-bit curve Curve448-Goldilocks (57-byte private key, 57-byte public key, 114-byte signature). Signing uses SHAKE256. Provides 224-bit security JWTs signed with EdDSA have a deterministic signature, meaning that the same JWT header & payload will always generate the same signature. This is deterministic in a good way, addressing the concern of relying on random nonce values to protect the private key. EdDSA only uses random values during private key creation. This is the algorithm that critics of JOSE & JWTs recommend.

Support for EdDSA in JWT libraries is a little patchy, but expect to see more of EdDSA soon.

Further Reading

HMAC (e.g. HS256)

HS256 = HMAC using SHA-256

Up until now, we’ve been talking about asymmetric cryptography, where only the token issuer has the private key to create the signature, and everyone else has the corresponding public key that can be used to validate the signature. For example, the identity provider has the private key and relying parties use a public key.

In the rare event that you will be the only person who issues and validates tokens, then you could consider using symmetric cryptography with something like HS256. This uses the same key to both create and validate a signature.

In my opinion, if you find yourself in this position, then I don’t think JWTs are the right solution for you. If the same entity is both reading and writing, then what is the requirement for round-tripping structured, plaintext data in a JWT? I would recommend storing the data in a database and passsing around a reference or to use something like a Branca token or JSON Web Encryption (JWE) to ensure only you can read the data.

Generally, using HMAC for JWT signing is seen as something of an anti-pattern.

Further Reading

None

none = base64 encrypted

Sorry, I couldn't resist. Please don’t use this.

Recommendations

Use EdDSA where possible and use ECDSA when it is not. If you are forced to use RSA, prefer RSASSA-PSS over RSASSA-PKCS1-v1_5.

I don’t think it’s a controversial statement to say that RSA is slowly on its way out. At the moment, offering ECDSA is a good alternative, but ideally, you’ll be wanting to move to EdDSA where possible.

But, no matter which algorithm you use, make sure you know ahead of time which algorithm to expect and which key to use for validation.

A huge thank you to Neil Madden for the technical review of this blog post.

Top comments (3)

Collapse
 
jillesvangurp profile image
Jilles van Gurp

If you are shooting for interoperability (which tends to be a goal with JWT) with various software stacks, service providers, etc., another thing to look at is what various libraries use. I use a Java library by auth0 which does not include support for EdDSA. It does use ECDSA, which is pretty universally supported. We use the ES512 variant. If Java is part of your stack, you'll likely end up using this library. There are a few other choices but this one is pretty popular.

Also, the EdDSA algorithms are not part of tools.ietf.org/html/rfc7518, which defines JSON Web Algorithms (JWA).

So, I'd avoid EdDSA unless you can handpick the libraries for each of your current and future services, clients, etc because there is a fairly large risk at least one of them won't support.

Collapse
 
scottbrady91 profile image
Scott Brady

Thanks for the comment 🙂

EdDSA is defined in RFC 8037 and is registered in the JOSE IANA registry. That's the requirement for being a JWT signing algorithm and to build upon the original JWA spec.

I agree, EdDSA is, unfortunately, not that widespread in the developer community right now. Which leads to my recommendation of "Use EdDSA where possible and use ECDSA when it is not".

Collapse
 
jillesvangurp profile image
Jilles van Gurp

Yeah my point is that is only the case when you can control who uses your tokens. When you can't, you have to assume at least some users will be unable to use EdDSA.

IMHO the cryptographic arguments for preferring that over ECDSA in the context of authentication/authorization use cases are not that compelling. When we are talking about protecting financial assets stored in a blockchain, you want the strongest thing humanly possible; which is probably why they picked it. Or put differently, if somebody is trying to break crypto to hack your JWTs, you have some interesting other problems to worry about probably.