1

I'm building a REST API resource server with JWT authorization for an Angular web app. I'm looking for a method to perform an additional check if the request body sent from the client application has not been modified by the attacker.

The communication between the server and the client is using SSL, but I need to make an additional check that the data sent in the POST request has not been modified by a man-in-the-middle attacker.

I think it should be solved by calculating the signature of the request on the client-side and then verifying it when the request goes to the server. Does anybody know how this can be done?

2
  • Certificate pinning is probably a good way to solve this. That ensures that only a certificate you consider valid will be trusted by the app, so it'd rule out MiTM which is able to install certs on the local device of the user. Against an attacker which can modify the app, all hope is up...
    – vidarlo
    Mar 2, 2021 at 20:17
  • Please edit your question to describe the client's environment, eg: browser, compiled application, etc.
    – brynk
    Mar 2, 2021 at 21:21

1 Answer 1

1

Before I answer this, I really need you to consider: what is your threat model here? Are you trying to be secure against a malicious user of the web app? Against somebody with the power to break TLS? Against a malicious browser extension? This is important, because the first and third are impossible and the second is almost certainly a waste of time. Is this just some best-effort-to-slow-them-down thing, like DRM, or are you expecting an actual security boundary here? Are you envisioning that, for some reason, the attacker can modify the request but not the response? If you're assuming the attacker has full control over the TLS tunnel - meaning they can inject malicious script into responses, as well as modify requests - this is flat-out impossible because there's nowhere you can hide a signing key that the attacker can't access and use to re-sign their modified message. For that matter, you also need to assume that the user has been able to previously communicate securely with the server for the purpose of key establishment; if that's not the case then the whole effort is also hopeless.


Signing the message body before sending is the right idea, but there are some problems you need to consider. Obviously each user needs their own key, tied to their account, so that the attacker can't re-sign the message with a different key. Additionally, the key needs to never be visible to the attacker, which means neither the key itself nor any value used to derive it can be transmitted in a request from the client.

Option:

  1. Session-based symmetric: At login, server generates a random symmetric key (128+ bits from a CSPRNG) and sends it to the client. Server also stores this key locally, in a session variable (tied to that exact user session). Client caches this key in some local storage and never directly sends it over the wire. Client also initializes a counter. For all subsequent requests, client takes the message body and computes an HMAC (probably something like HMAC-SHA256) over the message + counter, using the key. This HMAC is sent in a header or some such, along with the current counter value. Server verifies that the counter value is greater than any previously seen this session (storing the updated counter in its session state) and then retrieves the symmetric key from session state, computes the HMAC itself, and ensures it matches.
    • Fast to compute and verify.
    • Transmits the key, but only once, and only server-to-client.
    • Cryptographically simple, relatively easy to implement correctly.
    • Requires a common function for all requests, so that the counter is always used and incremented correctly and requests don't arrive out of order.
    • Requires server state, which is generally contrary to the goal of using a JWT and does not scale well to a cluster or even just a large number of active sessions.
  2. Stateless symmetric: As in #1, but the server keeps a secret symmetric key (shared across any cluster) and computes the session key by HMACing a string that the client will send unhashed with every request (could be the client's JWT, or even just part of it, as long as it includes the client identity and ideally some unpredictable value) with the secret key. Server still needs to send this session key to the client (and needs to do so every time it changes, if it does) but doesn't need to remember it locally. To avoid needing to store the counter value on the server, instead require that the client use a timestamp in the HMAC (and send it in plain text with the request), and the server makes sure that the timestamp is within some range.
    • Still pretty fast, though you now have to do two HMACs instead of one to verify each request (aside from any work that you have to do to verify the JWT, of course).
    • Slightly more complicated, but still reasonable.
    • Requires no additional per-user or per-session state on the server; suitable for clusters with only a little effort.
    • Introduces a single point of failure for all sessions, if the attacker can get access to the server's secret key then they can re-derive every victim's session key from their JWT.
    • Introduces the risk that the session key will rotate (when the JWT expires, as they are wont to do), which could invalidate legitimate requests. Can be mitigated by using some value that is still unique to the user and ideally to this particular session, and is sent by the client on every request, but does not change when the JWT is refreshed. This value could be part of the JWT or even just a random string chosen by the server and cached on the client during the session.
    • The use of timestamps rather than a counter or other proper nonce introduces a small opportunity for a replay attack, until the timestamp expires.
    • The use of timestamps requires that the server and client have well-synchronized clocks.
  3. Persistent, wrapped asymmetric: At user account creation or password change/reset, the client creates an asymmetric key pair suitable for signing (such as RSA or ECDSA). The client transmits the public key to the server. The client also hashes the user's password client-side, twice, with different salts and/or algorithms. One digest is sent to the server as the "password" for authentication (will receive additional hashing server-side before authentication, just like a normal password). The other digest is never transmitted, but is used as a symmetric key to encrypt (wrap) the user's newly-generated private key. The wrapped private key is transmitted to the server for storage. At login, the client performs the same dual client-side hashing of the user's password. If the server authenticates the user, it sends back the wrapped private key, which the client decrypts using the never-transmitted digest, and then stores client-side for the duration of the session. The server also retrieves the user's public key from storage. At all points that are neither logging in nor creating/changing passwords, the client uses its private key (which the server has never seen in unwrapped form) to sign the request body, along with a timestamp or counter/nonce (as desired; see considerations above). The signature, along with its timestamp/nonce, is transmitted in request headers. The server verifies the validity of the timestamp/nonce, and then uses the user's public key to verify the signature.
    • Unlike the previous approaches, THIS WORKS EVEN IF THE ATTACKER CAN READ ALL SERVER-TO-CLIENT RESPONSES although it does still require that the attacker NOT interfere during account creations / password resets.
    • Unlike the previous approaches, THIS DOES NOT EXPOSE THE USER'S PASSWORD AT LOGIN TIME; an attacker who intercepts the login flow will not be able to impersonate the victim as the attacker (until the attacker brute-forces the password from the authentication hash) because the attacker won't be able to unwrap the private key and hence can't sign messages correctly.
    • Highly complicated and prone to cryptographic (or ordinary implementation) error.
    • Requires the server look up the user's public key on each request, though doesn't require storing any actual session state (assuming timestamps are used).
    • Requires additional asymmetric operations on both sending and receiving a request; could potentially be computationally expensive to perform a lot of these in rapid succession (for the server's sake, choose a primitive that is not too expensive to verify signatures of).
    • STILL BREAKS if the attacker can MODIFY RESPONSES (attacker will just inject their own script to steal the private key and/or modify and re-sign the request at the client immediately before sending).
  4. As in #3, but use SRP (Secure Remote Password protocol) rather than the custom client-side hashing and key exchange, and use the computed key from SRP to sign (via HMAC; and heck, encrypt if you want to) the exchanges. Server and client both need to remember the SRP-derived key for the duration of the session, and never transmit it.
    • As with #3, is secure against an attacker fully breaching TLS on requests and able to monitor response plaintext.
    • As is normal for SRP, establishing the passwords (including handling resetting a forgotten one) can't be done within SRP and thus requires some other channel that the attacker can't be allowed to interfere with.
    • Requires storing server-side session state (the SRP derived key).
    • Can be used to provide an extra layer of encryption, not just integrity, to the channel (though you still need to be sure the attacker didn't ever inject malicious scripts at pre-login time or any other point where web content is transmitted in the clear).
    • Simpler to implement because you can use existing libraries for SRP, though still not trivial (there's probably a JS implementation of SRP usable in a browser out there already, but it's not built into the JS API library the way many primitives are).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .