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
.