6,663 karma · joined February 25, 2016
Previously AWS Cryptography (2019 - 2023).
Unless otherwise stated, my opinions are my own and do not reflect my employer.
https://scottarc.blog/about/
{
"my-super-cool-key-id": "some hs256 secret key string goes here",
"another-key-id": "-----BEGIN RSA PUBLIC KEY-----\n ... snip ...",
"yet-another-key-id": "----BEGIN EC PUBLIC KEY-----\n... snip ..."
}
And let's say you've got three different API endpoints, which each hard-code a specific algorithm (one HS256, one PS256, one ES256). But, because of the framework you're developing in, you're expected to provide a single configuration object containing a map of key IDs used by the entire application. (This is a common framework quirk.)What stops you from swapping out the kid in a JWT's header and getting the underlying library to use the wrong key type for the endpoint that accepts HS256?
{
"alg": "HS256",
- "kid": "my-super-cool-key-id",
+ "kid": "yet-another-key-id",
"typ": "JWT"
}
The answer varies per implementation. The JWT standards do not call this misuse potential out at all.Strictly listing the keys does not, at all, hard-code the algorithm those keys are used with in every possible programming language and runtime.
Some languages (Java) accidentally prevent this through type safety in the low-level crypto APIs. Others accidentally prevent this by not supporting the kid header.
I have yet to see a JWT library that deliberately prevents this misuse potential. Is that the library's fault? Or their defaults' fault?
EDIT:
The fix, by the way, requires doing this:
{
"my-super-cool-key-id": {
"alg": "HS256",
"key": "some hs256 secret key string goes here"
},
"another-key-id": {
"alg": "PS256",
"key": "-----BEGIN RSA PUBLIC KEY-----\n ... snip ..."
},
"yet-another-key-id": {
"alg": "ES256",
"key": "----BEGIN EC PUBLIC KEY-----\n... snip ..."
}
}
And then verifying that the alg for the key matches the alg for the token before attempting to verify the signature/MAC.Every JWT proponent says that, but it's a misuse that shows up in multiple libraries, in multiple languages, and isn't explicitly called out in the JWT Best Practices RFC at all.
I'm going to blame the standard for being error-prone.
There's nothing in any JWT RFC, to date, that calls out the need for cryptographic keys to be the raw key material in addition to its parameter choices, rather than just the raw key material. That's a fault of the standard.
That's not a single library's fault. That's the standard's fault.
PASETO has this to say: https://github.com/paseto-standard/paseto-spec/blob/master/d...
> As for the key ID attack, this sounds like it's just a trick to know where the private key is located? It shouldn't be publicly accessible.
This doesn't involve private keys at all. Look at the proof of concept code. https://github.com/firebase/php-jwt/files/6966712/php-jwt-po...
> Just pick a crypto scheme and the JWT is just an encoding that makes it easier to use.
That's not what JWT is, but I can understand why someone would be misled into believing that.
JWT isn't just an encoding format, it also includes a crypto algorithm negotiation protocol that lets the attacker choose the algorithm. Even if you strictly allow-list which algorithm you want to support, you can accidentally bypass this control in many libraries if you support the `kid` (key ID) header. [1]
It also allows attackers to completely strip the security. [2] [3]
Put shortly, JWT is a gun aimed directly at your foot. That's why there's so much hate for JWTs.
[1] https://github.com/firebase/php-jwt/issues/351
[2] https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba...
I think their reasoning is probably along the lines of "large RSA keys will buy you a little bit more time to migrate than small ECC keys if a quantum computer emerged today, due to memory constraints with what we think quantum computer will look like".
https://www.howmanydayssinceajwtalgnonevuln.com/
https://www.zofrex.com/blog/2020/10/20/alg-none-jwt-nhs-cont...
https://twitter.com/SchmiegSophie/status/1413248896227155968
https://twitter.com/tqbf/status/1414087907938377735
etc.
> * there is wide support for JWT. (See for example this IETF draft https://datatracker.ietf.org/doc/html/draft-ietf-oauth-acces... as well as the numerous libraries)
Yes, and my intention is to make sure there is wide support for PASETO in the near future too.
> Maybe paseto can eventually displace JWT, but I have a hard time seeing how that happens.
It won't ever 100% displace JWT in the same way that we won't ever 100% displace PHP 4 from the Internet, or the legions of badly written tutorials full of SQL Injection vulnerabilities that new programmers learn from.
The goal isn't to displace JWT, though. The goal is to provide a secure-by-default, easy-to-use, hard-to-misuse alternative.
After all, just because it's possible to implement a set of building blocks "securely" doesn't mean the kit is secure. I wrote more about this here: https://paragonie.com/blog/2019/10/against-agility-in-crypto...
But even if you don't care about all of that, the upcoming PASETO versions (v3/v4) offer cryptographic properties that JWT does not, such as exclusive ownership. https://github.com/paragonie/paseto/blob/master/docs/Rationa...
------
Also, in case it wasn't obvious: PASERK is still a work-in-progress; things might change. Wait until you see a `1.0` tag before trying to implement it.
There is a reference implementation in PHP, but that's also experimental and no stable release has occurred there yet.
By updating all of the old, insecure tutorials to redirect towards secure answers instead.
Details here: https://paragonie.com/blog/2018/01/our-ambitious-plan-make-i...
ISO is somewhat unique in requiring you to pay money to even know what the standard says before you can ever think about being in compliance.
Most other standards organizations (i.e. NIST, IETF, W3C) do not have this weird financial burden, so merely "Publicly Available Standards" doesn't sell the significance of this.
Making their standards publicly available is a good move by ISO. I hope we see more of this.
Modern cryptography is baked in since 7.2.
https://libsodium.gitbook.io/doc/bindings_for_other_language...
Most of the people who shit on PHP have a lot of love for other languages. A survey of the cryptography features the "favored" languages offer will almost certainly fall into two camps:
1. "We wrap OpenSSL"
2. "Go compile it yourself" (i.e. there is nothing baked in)
There's a lot of badness with OpenSSL's API design, especially with asymmetric cryptography. For a fun exercise in these languages, try encrypting with RSA with OAEP padding, but without using SHA1 as your hash function.
For completeness, PHP is one of the languages that wraps OpenSSL too! But it also wraps libsodium, and the community has been moving towards libsodium (unless they need something from OpenSSL for the sake of backwards compatibility) since early in the 7.x series.
If you're going to provide cryptography features in your language, but you aren't shipping modern cryptography in your standard library, you're underperforming what PHP has offered for years at this point. The easiest way to meet the standard that PHP 7.2+ establishes is to add libsodium to your language's standard library.
(There are salient arguments for "why even provide a cryptography feature as part of the language at all?" but most of the languages that see real world deployment are already doing that.)
https://paragonie.com/blog/2016/10/guide-automatic-security-...
But then there was this: https://twitter.com/NatSecGeek/status/1273329710576152581
https://tonyarcieri.com/on-the-dangers-of-a-blockchain-monoc...
https://paragonie.com/blog/2017/07/chronicle-will-make-you-q...
> Not if zone operators upgrade to ECDSA as defined for DNSSEC in https://tools.ietf.org/html/rfc6605
First: That's a big "if". Lots of RSA legacy support.
Furthermore, ECDSA is so bad that Ed25519 and Ed448 are even coming to FIPS 186-5 later this year.
Citing ECDSA adoption in DNSSEC doesn't make as strong of a case as you might think.
https://www.camao.one/de-de/referenzen
Unless they have some other cards they haven't played yet, this sounds more like a headline intended for investors rather than customers.
Of the first programming languages that cater to novices to some degree (which is important to project health, because excessive gatekeeping is toxic), PHP was the first to make Curve25519 and Argon2id widely available (through the sodium extension). (7.2)
Even with the "just use FFI" mindset of other languages, the experience is janky at best.
For example: Before sodium-plus [1] came along (which was something I created, so I'm removing it from the table for my criticism of the JS crypto ecosystem), JavaScript required knowing which of the following packages to install: [2] [3] [4]
There was no guidance available anywhere. Some of these APIs were suitable for browsers, others for mobile devices, and still others for server-side JavaScript. And their APIs were subtly incompatible with each other.
Java's in a similar situation [5] today.
> Java (Java Native Access): libsodium-jna
> Java (Android): Lazysodium for Android
> Java (Android): Libstodium
> Java (Android): Robosodium
> Java (Android): libsodium-JNI
> Java: Apache Tuweni (crypto module)
> Java: Lazysodium for Java
> Java: jsodium
> Java: Kalium
Yikes.
With PHP7, you just had to update to the latest version (and make sure your OS package vendor isn't huffing or eating glue, which was rare but still existent) and you had it.
There are probably still languages and runtimes today that make using modern cryptography (Ed25519, X25519, etc.) a miserable experience.
Miserable experiences moving to modern cryptography keep peoples' projects trapped in an early 2000's RSA-PKCS#1v1.5 + AES-CBC hellscape reminiscent of SSLv3 but somehow worse.
Thus, I would argue that what PHP did with libsodium counts as revolutionary.
Your move, $languagesThatHackerNewsFindSexierThanPHP
[1]: https://github.com/paragonie/sodium-plus
[2]: https://www.npmjs.com/package/libsodium-wrappers
[3]: https://github.com/sodium-friends/sodium-native
[4]: https://www.npmjs.com/package/sodium
[5]: https://libsodium.gitbook.io/doc/bindings_for_other_language...
Instead of cipher agility, PASETO uses versioned protocols.
My DEFCON Crypto & Privacy Village talk (slides and YouTube video at https://paseto.io for the curious) covered this distinction in detail.
But before I resuscitate PASETO, XChaCha20 needs an RFC. It's pending IRTF kick-off.
If you need any other validations, it's entirely up to you to specify what those are.
You might care about checking `exp`, but I might instead care about `iat` with a hard-coded token lifetime.
PASETO supports whatever zany business logic developers need, but doesn't enforce anything by default.
But once you add a rule to your parser, it will fail-closed on those rules.
> A token without timestamp will never be valid if a rule for checking timestamp is added in the parser?
Correct.
If your parser is set to check timestamps and one is invalid or omitted, it throws an exception.
They're not required (as they weren't in JWT). If you want to use them, use them. The parser will validate them if you want it to.
https://github.com/paragonie/paseto/blob/master/src/Rules/No...
https://github.com/paragonie/paseto/tree/master/docs/02-PHP-...
If you want ultra-simplicity and don't need clearsigned tokens (i.e. signed by a third party), you can get away with just using Branca. PASETO covers both use cases.
If you're going to put in the work (which I am), you might as well start with a clean slate rather than trying to piecemeal security improvements into their design-by-committee spec.
Better options for PGP use cases:
- AWS Encryption SDK: https://docs.aws.amazon.com/encryption-sdk/latest/developer-...
- age https://age-encryption.org
- Magic Wormhole https://github.com/warner/magic-wormhole
- NaCl/libsodium (and/or usability wrappers) https://libsodium.gitbook.io/doc/
Better options for JWE use cases:
- PASETO: https://paseto.io
- Branca: https://branca.io
> How so? I'm still learning this stuff, so I'm genuinely curious.
https://paragonie.com/blog/2019/10/against-agility-in-crypto... :)
The end result depends entirely on the behavior of JavaScript's Array.indexOf() implementation.