paragonie / paseto

Platform-Agnostic Security Tokens
https://paseto.io
Other
3.23k stars 108 forks source link

public payload isn't encrypted #115

Closed shaicoleman closed 2 years ago

shaicoleman commented 4 years ago

First, thanks for creating Paseto.

I found it to be a really surprising aspect of Paseto that even though it's built on top of modern cryptographic libraries and is designed to be a secure replacement for JWT, it doesn't actually encrypt the data.

This is something that someone can seriously shoot themselves in the foot, because it's not secure by default, and when I think about "public", I associate it with encrypted public key cryptography, e.g. GPG/SSH. Instead of "public" calling it "unencrypted-signed" would be a better name to highlight the danger of using it.

This really limits the use cases of Paseto for, specifically any kind of server to server APIs which contains private/secret data, or semi-trusted server environments.

Paseto gives 4 options, and none of them are secure by default

v1.local and v1.public use outdated cryptography. I don't think anyone who is adopting Paseto will choose to use them. v2.local requires to share the private key v2.public doesn't encrypt the data (somewhat like alg: "none")

I would think it would be much better to only have one option (v3), using encrypted public-key cryptography, and deprecate the other options. This would significantly simplify the standard, yet fulfill more use cases in a more secure manner. This is the approach taken by Branca.

This gives you flexibility of either encrypting data in the payload, or just signing data in the footer, or a combination of both.

libsodium is pretty much ubiquitous at this point, and v1 is not interoperable and thus fails to serve as a lowest common denominator, as many libraries don't support it.

Regarding https://github.com/paragonie/paseto/issues/83#issuecomment-404942554 My understanding is encrypt+sign is not secure, as someone can replay the encrypted part. While sign+encrypt does not allow to have an unencrypted footer.

I think the best approach would be to sign+encrypt+sign. i.e. sign the payload, encrypt it, and then sign the entire message, including the unencrypted footer.

http://world.std.com/~dtd/sign_encrypt/sign_encrypt7.html#tth_sEc5.2

Mythra commented 4 years ago

Hey @shaicoleman ,

Figured I'd drop my thoughts here as well (so we have more than one opinion on this thread 😄 ).

I found it to be a really surprising aspect of Paseto that even though it's built on top of modern cryptographic libraries and is designed to be a secure replacement for JWT, it doesn't actually encrypt the data.

It does have a mode for encrypting the data, but yes one mode keeps the data public hence the name "public".

This is something that someone can seriously shoot themselves in the foot, because it's not secure by default, and when I think about "public", I associate it with encrypted public key cryptography, e.g. GPG/SSH. Instead of "public" calling it "unencrypted-signed" would be a better name to highlight the danger of using it.

Are we expecting people to not to assume the thing called public would at least clue them into reading the documentation that the data inside is public? Also I feel danger is overstated here since it is still signed, most people I know of don't stick sensitive fields inside of a JWT. Perhaps you know of some examples where the data can't be shown to the user? Most cases I'm aware of the data actually should be public since it's for say a frontend to encode the UserID/Other User Info for API calls, but also still have it authenticated by the backend.

This really limits the use cases of Paseto for, specifically any kind of server to server APIs which contains private/secret data, or semi-trusted server environments.

Does it limit it though? What cases would shared key cryptography not work, to the point you'd want public key crypto (not signing)? Even if using public key crypto you need to know the encryption key to encrypt it for, and everyone who wants to decrypt it is going to need the key. So if you have more than one server in a service you're gonna need to share that key anyway, so why not have it share just a simple string?

Paseto gives 4 options, and none of them are secure by default

To go from "this library doesn't offer a mode for my specific use case" to "this library is insecure by default" is quite a big jump, and I don't think quite justified here.

v1.local and v1.public use outdated cryptography. I don't think anyone who is adopting Paseto will choose to use them.

As mentioned in the Version One Spec it is there specifically for compatibility mode, or people who need it. We specifically have had to use it at companies I've worked for before for government systems that required FIPS builds. OpenSSL was incredibly easy to get FIPS certified builds of where as LibSodium is not (it requires a lot to get it certified, and recertified every time it updates). Like it or not, the cryptography is still needed in those environments. Plus the algorithm choices it uses while not modern (and not the most ideal) aren't broken. So to call for a break for them would be needless, and severely limit potential users.

Like it or not, many more systems are legacy rather than entire greenfield development. Allowing them to slowly upgrade to more modern crypto is a good choice.

Also to circle back to "none of them are secure by default", again none of the things are broken. Just because they aren't "modern" doesn't mean they should be excluded.

v2.local requires to share the private key

Again I sort of scale back to my earlier point. How is this a problem of making it "insecure" by default? Many secrets are like this (db passwords for example). Plus even if we used PKI, if you had multiple nodes behind a load balancer something very common, you'd still need to share the private key in a public key crypto system here. What benefits does public key crypto bring here that we'd want?

You can even emulate a "unique key" for each service by using a key-id in the footer. From there you can then rotate the shared secret, and have a unique one per service. Adding PKI adds overhead that I don't think I see a benefit for. Though I'd love to hear how it could help.

v2.public doesn't encrypt the data (somewhat like alg: "none")

No, but the data is signed. It can't be tampered with. Which is incredibly useful for things like OIDC (which are some of the biggest users of JWT), where user information needs to be passed to the frontend on login, but the backend also needs to validate that it hasn't been tampered with so you can't just assume any user.

FWIW this exact example is also called out in the README, and is an incredibly useful usecase that would need to be supported for anything that wants to be used in the same way as JWTs.

I would think it would be much better to only have one option (v3), using encrypted public-key cryptography, and deprecate the other options. This would significantly simplify the standard, yet fulfill more use cases in a more secure manner. This is the approach taken by Branca.

Again this would hurt it from actually ever becoming a replacement for JWT (deprecating signing), and in local keys it's much easier to share those on infra rather than one private key for every single node. I think this would simplify it sure there's only one option, but would make a lot of use cases in use to day impossible (or harder, by having to move private keys around rather than simple strings).

If Branca works better for your use case it sounds like you should use Branca. Reading the specifications it seems to make a much different set of trade-offs which if they work better for you, that's great. Use it.

I'd still be interested in hearing about public key cryptography on multiple machines though (e.g. how does it help us?), but I think optimizing the library around a single use case, and ignoring the use cases we were built securely for today would be a mistake.

This gives you flexibility of either encrypting data in the payload, or just signing data in the footer, or a combination of both.

Forcing the users to manually sign data sort of gets us back to square one. You're now writing some structured data into a footer (the message plus the signature), so you now have to implement parsing/signing in your own way. Which may be different between vendors, and gives you the chance to shoot yourself in the foot since it could in theory let you sign anything in anyway.

Also what about the OIDC use case where all the data is public. Forcing the payload to encrypt an empty string seems like a waste of time, and an ill-design choice for a core use case for JWTs.

libsodium is pretty much ubiquitous at this point, and v1 is not interoperable and thus fails to serve as a lowest common denominator, as many libraries don't support it.

See above, but OpenSSL is still required in some environments (notably ones that need things like FIPS compliance), even if it's undesirable (like pretty much all of FIPS is imo).

Regarding #83 (comment) My understanding is encrypt+sign is not secure, as someone can replay the encrypted part. While sign+encrypt does not allow to have an unencrypted footer. I think the best approach would be to sign+encrypt+sign. i.e. sign the payload, encrypt it, and then sign the entire message, including the unencrypted footer.

I think #83's comment actually really sums this up well. Sure we could support public key crypto, but almost no one needs it. This seems to be the situation you're in, and while it's interesting to see if maybe we can support it easily, it's also something rarely used, and optimizing the entire protocol around it is silly. Since it isn't the majority use case for what we're replacing, JWT. As opposed to Branca which while it can be used in a JWT sense, optimizes for something more akin to what you're looking.

shaicoleman commented 4 years ago

OpenSSL is still required in some environments (notably ones that need things like FIPS compliance), even if it's undesirable (like pretty much all of FIPS is imo).

I understand the FIPS requirement usecase, although it's not applicable for most of the world, and I don't think it's the right choice to design an internet standard around that. In cryptography, reducing choice makes for more secure software.

The only version of OpenSSL that supports FIPS became unsupported as of 31/Dec/2019. I think a better solution would to have the FIPS mode as a separate extension or a separate standard (Paseto-Legacy), and remove it from the main standard. Also, when OpenSSL 3.0 gets released (expected Q4 2020), it will support modern cryptography with FIPS, so perhaps libsodium wouldn't be a requirement.

No, but the data is signed. It can't be tampered with. Which is incredibly useful for things like OIDC (which are some of the biggest users of JWT), where user information needs to be passed to the frontend on login, but the backend also needs to validate that it hasn't been tampered with so you can't just assume any user.

Just for the specific example you mentioned of OpenID connect, it would be much more secure to have the email encrypted in the token, for privacy and security and compliance purposes (e.g. GDPR). Email is personally identifiable information and needs to be encrypted. Also, I wouldn't want to leak database IDs, one time tokens, etc. A secure approach would be to encrypt things by default, with the option to opt out encryption for specific fields by putting them in the footer.

Unencrypted data is not a secure default.

Again I sort of scale back to my earlier point. How is this a problem of making it "insecure" by default? Many secrets are like this (db passwords for example). Plus even if we used PKI, if you had multiple nodes behind a load balancer something very common, you'd still need to share the private key in a public key crypto system here. What benefits does public key crypto bring here that we'd want?

Just because you use public key cryptography, doesn't mean you have to give each node a different key. You can share the public key across multiple nodes if you wish to keep things simple. The difference is that if one server gets compromised and they have just the public key, an attacker can't generate new tokens, thus greatly reducing the attack surface. There's a good reason why things like TLS/SSH/GPG use public key cryptography.

What if you start have things on the same server, and then you need to separate it out to a different one? Again, symmetrical encryption is not a secure default, because once you compromise one server, you can now compromise others as well.

Again this would hurt it from actually ever becoming a replacement for JWT (deprecating signing), and in local keys it's much easier to share those on infra rather than one private key for every single node.

This doesn't deprecate signing, it just gives you choice whether to encrypt it or not. The payload is encrypted, the footer isn't. If you want to have things just signed, you just place the data in the footer.

The problem is if I need to add encrypted information to the token, I don't have a good solution with the current standard.

None of these options are good.

If Branca works better for your use case it sounds like you should use Branca. Reading the specifications it seems to make a much different set of trade-offs which if they work better for you, that's great. Use it.

The downside of Branca is that everything is encrypted, and you can't include unencrypted information as with Paseto-local, and a lot of things are left unspecified.

aidantwoods commented 4 years ago

If I'm reading this right, the main problem you're trying to solve here is that you want to encrypt the contents of your payload but want to do so with asymmetric crypto (as opposed to a symmetric which is already possible)?

Maybe we can separate the other points into their own discussions? (e.g. concerns about the existence of v1, naming of modes, and preferred signature construction). These are all fine as discussions to have, but I think we're more likely to arrive at answers if we can focus the dialogue a bit.

shaicoleman commented 4 years ago

Yes, that's the main thing, encrypt the payload with asymmetric crypto

mikk150 commented 4 years ago

Yes, that's the main thing, encrypt the payload with asymmetric crypto

I was actually also looking replacement for JOSE(specially asymmetric keys and JWE part, so asymmetric key crypto)

So +1

paragonie-security commented 2 years ago

This is solved in PASERK. (PHP implementation in the works.)

What you can do is this:

  1. Generate a random PASETO vX symmetric key (where X = desired version).
  2. Encrypt the random key using PASERK's kX.seal type with a target asymmetric public key.
  3. Use vX.local to encrypt the tokens, using the kX.lid of the serialized key (from step 2) in either the footer (all versions) or as an implicit assertion (versions 3 and 4) to bind the PASETO token to the PASERK serialized key.

You can safely reuse a serialized key for multiple token, as long as the serialized key is communicated, at some point, to the decryptor.

paragonie-security commented 2 years ago

PASERK Reference Implementation -- check out the example in the README.

Since this is solved by PASERK, I'm going to tenatively close this issue. The public comment period for PASETO v3/v4 ends next week, after which we'll focus on getting PASERK's API stable.