4

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), where d is private to signer, n is given to verifiers, and e is well known. Notice that this is an unusual use of RSA, where n 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 signature
  • claims = 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.

fgrieu
  • 140,762
  • 12
  • 307
  • 587
Rob
  • 349
  • 1
  • 12

2 Answers2

3

What's asked is met by a “signature scheme giving message recovery” used with “total recovery”, that is with empty “non-recoverable message”†. Then the whole message is embedded in the signature, and remains unintelligible until the better part of the signature verification job has been made (of course it's always possible to skip some check in the signature verification, but at least that's unlikely to happen by accident).

For an RSA-based standard and short messages, there is ISO/IEC 9796-2:2010‡, schemes 2 or 3, with empty “non-recoverable message”. There's an implementation in BouncyCastle as ISO9796d2PSSSigner. If being broken under chosen-messages attack is not to fear, we can consider scheme 1, originally in ISO/IEC 9796-2:1997, commonly used in bank cards and often available in JavaCards. It's described in EMV 4.4 Book 2 section A2.1 with a restriction to byte-aligned messages and keys making it simpler than the standard, but only in “partial recovery” mode which opposes “total recovery”, and for a 20-byte hash (specifically SHA-1).

To remove the message size restriction, we can encrypt the message with a random key (AES-128-CTR will do), and sign with a “signature scheme giving message recovery” the concatenation of the key and ciphertext (in order such that the key is in the recoverable part of the message). ISO/IEC 9796-2 can be used, but in this application I recommend against scheme 1, especially with a hash shorter than 512-bit.

For something (EC)DLP-based and no restriction on message size (because the signature itself uses a cipher much as in the above paragraph), we can use ECPV of ISO/IEC 9796-3:2006‡ with empty “non-recoverable message” MCLR, or ECPVS of ANSI X9.92-1-2009‡ with empty “visible message” V. These standards are based on Pinstov and Vanstone's Postal Revenue Collection in the Digital Age, in proceedings of Financial Cryptography 2000‡, albeit without the per-device key derivation: we skip section 4 and start section 5 with the (public, private) key pair $(Q_A,a)$ obtained as $Q_A=a\,\mathsf{P}$.

Note: Pinstov/Vanstone signature is designed to use redundancy in the recoverable message (e.g. restriction to printable ASCII, or valid JSON) for utmost compactness. If that's not wanted (because the message is arbitrary, or otherwise a robust redundancy-checking method is not available, or we just give up on compactness), we can make some redundancy by appending say 128 bit of zeroes to the recoverable message, and check them on signature verification.

Note: the adoption of Pinstov/Vanstone signature has been low, perhaps because it was patented. But my understanding is that whatever (possibly) relevant IP has expired. At least, anything disclosed in the aforementioned Postal Revenue Collection in the Digital Age paper of FC 2000 is free to use now.


† The “non-recoverable message”, also known as “clear message” or “visible message”, is a fraction of the message that appears in clear along the signature itself in a “signature scheme giving message recovery”. This opposes to the “recoverable message”, that is the part of the message embedded in the signature, and obtained by the verifier only as a byproduct of signature verification.

‡ Paywalled

fgrieu
  • 140,762
  • 12
  • 307
  • 587
  • 1
    Can we do a canonical Q/A for signature with message recovery? Standards and practice? – DannyNiu Mar 22 '23 at 00:49
  • @DannyNiu: Such Q could be several things. For "what is SMR" this is close, albeit with an ISO/IEC 9796-2 orientation. Perhaps we are missing "Benefits or SMR", "where is SMR actually used"; "standardized SMRs" or/and "theoretical state of the art in SMR" (these differ for integer-Factorization-based schemes, and exist in a state of limbo for (EC)DLP schemes and for Ed25519). I'm also thinking of "cryptography for QR codes", but that's huge. – fgrieu Mar 22 '23 at 20:40
  • 1
    This is the implementation I created of this idea. It actually is not possible to get the plaintext claims without doing a signature check; in the same way that it's not possible to get the plaintext of encrypted data without a secret key; as I made the decrypt a function of the sig check. In the hands of a web developer (plaintext, Sig(key,plaintext)) is a bonafide hazard. The attacker is the web developer.

    https://github.com/rfielding/whiskeyTango

    – Rob Mar 23 '23 at 20:29
  • 1
    @Rob: had a look at the scheme, which verification procedure is nicely drawn there, with signature just above. I have reservation on the security of the RSA signature part, which (as far as I understand from that) is textbook RSA signature of a narrow witness of the message. This is subject to the Desmedt and Odlyzko attack. In a nutshell, RSA signature should be on representatives nearly as wide as the public modulus. Independently: RSA private key is not $(n,e,d)$ in practice. – fgrieu Mar 24 '23 at 03:23
  • @Rob: with a 256-bit hash, the attack would require to obtain so many signatures that it might not be practical. But it's still worrying, and shows that the scheme as is can't have a security reduction, required for today's new schemes. Independently: I had to go to implementations to determine there is a width check on sig^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:20
  • definitely the sig^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:37
  • It is my belief that almost all of the hazards with RSA in practice are due to using a small e as an optimization. e and d can be interchangable, and of same size. I think it's important to stress that I am not using small e by default, using "symmetric" rather than "public" key. – Rob Mar 25 '23 at 13:39
  • 1
    @Rob: yes $e$ and $d$ can be made interchangeable, and the only good reason for small $e$ is performance. But it's not true that almost all of the hazards with RSA in practice are due to using a small $e$. Many attacks still work with random large $e$; and among these the Desmedt and Odlyzko attack that (at least, about) works against your system. For more on why $e=3$ is frowned at, and why that's often for wrong reasons, see this. I made a question about attacking the RSA part of your signature. – fgrieu Mar 25 '23 at 14:16
  • It's this line of Go exactly: new(big.Int).Exp(b, x, n), for b^x mod n – Rob Mar 26 '23 at 05:02
  • 1
    @Rob: yes, got that, the code is pretty understandable. My remark "seemingly 512 bits in golang" is about if len(V.Bytes()) > 64 where I think 32 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 (your V) being 256-bit rather than (about) as wide as n, 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:37
  • So, I believe that what is essential about what I am doing is to sign a value other than the plaintext hash. if sig^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
  • 256 bit being too narrow is the main issue that enables a near-practical attack (said to require some $2^{69}$ something, can't tell yet if it's known signatures or "just" work for the attacker). In theory, the issue is checking that what's on the left of the random 256 bits is constant. Making that a wide hash of the 256 bits would fix it and make your signature essentially RSA-FDH, which has a security proof. – fgrieu Mar 29 '23 at 05:19
2

If you use RSASSA-PSS, then there's a "salt" that must be recovered to verify the signature. I'm not sure if this would be secure, but this salt may be used as key or IV to a symmetric-key cipher to decrypt your data.

Reference: PKCS#1 v2.2.

DannyNiu
  • 9,207
  • 2
  • 24
  • 57
  • 1
    Yes, but we would need to modify the implementation of RSASSA-PSS to make salt an input and output, and sign the ciphertext. This requires that salt generation is in a step separate from signature production. – fgrieu Mar 21 '23 at 09:53
  • I'll have to second @fgrieu 's comment - don't break the implementation that's provided to you. – DannyNiu Mar 22 '23 at 00:46