Can somebody lead me to existing literature on making the extraction of plaintext dependent on a valid signature check first? I didn't think it was possible to force signatures to be checked; but this is possible, and I have an implementation of it with RSA.
For background: I have been following the multiple problems with JWT tokens, that manage to turn a common cryptography practice into a very real hazard.
signedData = {plaintext, Sign(Hash(plaintext))}
Until recently, I assumed that you can't force anyone to perform a signature check; and that you should trust the client to defend itself with a correct implementation. But the JWT standard does this:
B64(header) + "." + B64(plaintext) + "." + B64(signature)
The problem with this is that the sort of developers that deal with these tokens will pick the shortest path to getting things done, possibly unaware of bad consequences. So, when they get a JWT token, inevitably:
UnB64(split(token, ".")[1])
extract the claims, and ignore the signature, because this works.- Allow header to specify trivial-to-forge algorithms like
None
. That also works. - Use the value in claims.issuer to look up a missing trust. This way, you can "perform" the signature check, while defeating the purpose of doing the check; because you dutifully downloaded the trusted public key at the behest of the attacker. This makes untrusted signer errors go away.
So, the way out of this is for the claims to be encrypted. But you need proof that you trust the key id (kid
), which may be public only to those allowed to decode the tokens. Instead of a kid
value in the header, just make kid
the only thing in the header at all.
- RSA key:
(n,d,e)
, whered
is private to signer,n
is given to verifiers, ande
is well known. Notice that this is an unusual use of RSA, wheren
is not completely public. - You need a witness (random value
k
) to doing steps correctly to decrypt:E = AESEncrypt(k,claims)
- Prove that you checksummed the token ciphertext:
Hash(E)
- Proof of a verified signature:
V = Xor(k, Hash(E))
- Signed proof of verified signature (RSA):
Sig = (V^d)%n
- Token is similar to JWT, but incompatible to stop JWT library use:
Token = {kid,E,Sig}
Verification:
- Token is verified by looking up public key via
(n,e) = trusts[kid]
. k = Xor( H(E), (V := (Sig^e)%n) )
Proves that we actually validated signatureclaims = AESDecrypt(k, E)
This acts as an encrypted token, where the RSA public key is not completely public. It is now impossible to get the claims without verifying the signature first. This fixes the primary misuse of JWT, because bad implementations will only create gibberish. This also resolves the issue of claims that should be secret except to signer and the verifiers.
Real JWT libraries enable every stupid thing imaginable, from automatically using claims.issuer
to download public keys for trust over https; to extracting claims without verification, to enabling algorithms that the client is not expecting such as the infamous signature algorithm alg:None
.
https://github.com/rfielding/whiskeyTango
– Rob Mar 23 '23 at 20:29sig^e mod n
, thru raising an OverflowError in python over 256 bits (with no proper diag), and seemingly 512 bits in golang – fgrieu Mar 24 '23 at 04:20sig^e mod n
is just Bignum arithmetic; "Textbook RSA". This is the one step where it might be possible to make things worse, and honor a forgery if something went wrong. I think overall, this cant be worse than JWT as it is. – Rob Mar 25 '23 at 13:37e
as an optimization.e
andd
can be interchangable, and of same size. I think it's important to stress that I am not using smalle
by default, using "symmetric" rather than "public" key. – Rob Mar 25 '23 at 13:39new(big.Int).Exp(b, x, n)
, forb^x mod n
– Rob Mar 26 '23 at 05:02if len(V.Bytes()) > 64
where I think32
was intended, and is not visible in the verification drawing. I assume that's fixed. My remark about the Desmedt and Odlyzko attack being (borderline) feasible remains. It's about the random-like message representative (yourV
) being 256-bit rather than (about) as wide asn
, which is a condition for security: RSA is (assumed) secure for random message in $[0,n)$, and we can reduce that a little, but not down to $[0,2^{256})$. – fgrieu Mar 26 '23 at 07:37sig^e mod n
is insufficiently secure; i don't know what it is, exactly, that should be different. I am certain from experience, that letting web developers discover plaintext before checking the signature is a fatal flaw for a web token. Is it that i am signing a number that's too small? only 256 bit number? if that's the case, the random value k could be more bits. – Rob Mar 29 '23 at 02:45