How to strongly secure public APIs using HMAC

Sajjad Rad
7 min readSep 3, 2023
Generated by DALL-E

There is no general authentication method for all types of API. It depends on the API purpose. The API might provide sensitive services like managing values (money). Any mistake can make a mess with the users’ assets. So, the authentication and authorization methods must be chosen wisely depending on the use case.

API Key

API Key itself can not be used in sensitive architectures. It’s an unsecured method to authenticate beyond the internet. The secret key (API Key) is presented in every request and can be exposed. It’s useful for analytic or rate-limiting purposes. Read this post from Google Cloud documentation to learn why or when to use the API key approach.

HMAC Authentication

Hash-based message authentication code (HMAC) is a method to calculate the message authentication code using a hash function in combination with a secret key. It is defined by RFC 2104. With HMAC authentication, no secret is presented in the request.

Secret Key

The API returns a pair of Keys, including ID and Secret, while creating a new user (Access Key and Access Secret here). This Secret is used to calculate the HMAC of each request. The Access Key is public and helps the server find the secret to recalculate the HMAC.

Signature

Each request must be signed using the Secret. The signature proves that the holder of the Secret requested the resource without using the Secret directly in the request. It guarantees the authenticity of the action.

The data to sign is a combination of the following:

  • HTTP Verb (GET, POST, PUT …)
  • Content-MD5: MD5 hash of the request body (for example, JSON content)
  • Content-Type
  • Timestamp
  • Request URI
  • Nonce: a random number to make the signature different each time. This option secures the authentication from replay attacks.

Why the request URI and HTTP Verb are included in the signature?
Including the HTTP verb in the signature’s data helps to have a different HMAC per verb. For example, consider not including the verb in the HMAC calculation. Thus, we have an identical signature for all verbs. The victim client sends a request to GET the resource. Then, the attacker uses the same signature to DELETE the resource before the expiration!

Nonce
As mentioned, the nonce is a random and unique number to make a different signature each time. Without using the nonce, the attacker can replay a request. Using a mix of Nonce and timestamp, strictly secure your API from reply attacks. [REF]

The service must cache the nonce number to stop serving a request with a duplicate nonce in the allowed timestamp window (for example, 5 seconds). The timestamp is used to expire the request, but the expiration time can not be too short because of time sync errors in different machines and network delays.

Flow
The client concatenates the mentioned elements with the “\n” character (newline, which means the Unicode code point U+000A ) together. Then, it calculates the HMAC of the data using the Secret, called the digest. Finally, the client encodes the digest to Base64 to use it in the header.

The server parses the Authorization header and uses the access key part to find the client’s secret. Subsequently, the server calculates the request body MD5 and uses it to recalculate the client signature. If the resulting signature equals the signature in the request authorization header, the server processes the request; otherwise, it drops the request and returns an authentication error.

stringToSign = HTTP-Verb    + "\n" +
Content-MD5 + "\n" +
Content-Type + "\n" +
Timestamp + "\n" +
RequestURI + "\n" +
Nonce;
signature = Base64(HMAC-SHA256(SECRET_KEY, stringToSign))header = "Authorization: TXC-HMAC-SHA256 <ACCESS_KEY>:<SIGNATURE>"
header = "X-TXC-Nonce: <NONCE>"
header = "X-TXC-Timestamp: <TIMESTAMP>"

Disadvantages

  • The implementation is more complicated.
  • Any header or body injection and modification by libraries ruin the content MD5 and signature.

Nonce cache and request expiration

As we discussed, we need to cache the nonce until the request expires to prevent serving a request with a duplicated nonce. If we don’t, the attacker can replay the request. Also, caching the request nonce is expensive. Storing and reading add more latency and can also be the point of failure.

It’s reasonable to consider 5 seconds for timestamp expiration (due to timestamp error on different machines and network delay) and cache the nonce for that time window. When the nonce cache is deleted, the timestamp is expired, and the same nonce can not be used. Also, after the request expiration, if a duplicate nonce is used, the timestamp value must be different, and the HMAC signature will be different. This is what we want!

Using Secure Channel

The client and server must communicate over a secure channel, like using TLS. Thus, ensure the client is not mistakenly calling the API server over HTTP. HTTP requests are vulnerable to man-in-the-middle attacks, which can result in the exposure of the request body.

You might ask why we would care for exposure of the request over HTTP? The request has no secret (like the Bearer JWT token). In the HMAC concept, you are right! No secret is transmitted using HMAC, and it’s secure to use without TLS because nonce and timestamp protect it from reply attacks. But! the man-in-the-middle could read your request body, which might include some sensitive information. For example, a withdrawal request from an account to an external wallet address.

These are some actions to enforce using a secure channel:

  • Redirect HTTP to HTTPS on the edge.
  • Use “HTTP Strict Transport Security” header to prevent browsers from calling the API over HTTP.
  • Other API callers (like HTTP clients) are still vulnerable to the attack while using the “HTTP Strict Transport Security” header. Because it only enforces the browsers, not other HTTP clients. So, if an SDK is published, ensure it sends the request over HTTPS on the production environment.
  • A threat detection mechanism is beneficial to monitor and trigger an alert or report when API is called with a request carrying secrets over HTTP or an insecure channel. So that API keys could be revoked or disabled easily for security reasons.

Some Security Considerations

Public API vs. Private API

It’s much easier to make the API more complicated on private APIs. You can use different terminology or nicknames for cryptography algorithms to make it harder for attackers to understand the API. For example use Red for SHA-256 and Blue for SHA3-512 . It’s not a cure, but it makes it painful.

For public APIs, an SDK with public documentation is published. So, the attackers have the blueprints of the Bank.

Brute Force, Attacks, and API Abuse

The attacker can start to guess the Secret of a particular wallet ID by spamming with some random string (I know it isn’t effortless).

In this case, the victim’s wallet could be suspended for security reasons. But any similar attack results in blocking the user’s account. For example, the user’s business competitor can use this attack to block the wallet and the payment gateway.

Some tricks can be used to slow down the attacker. Like returning the auth error with some random delay or returning the 200 Status code for authentication error! Right! To fool the attacker.

reCAPTCHA v3 has a solution for APIs! The client sends a request to the reCAPTCHA server to get a token. Then, it uses the token to send the request to the API server. The API server validates the reCAPTCHA token before processing the request.

Rate-Limiting

The rate-limiting also is a good solution in most cases. Limiting the API request rate by sender IP is the simplest way Cloudflare supports for free! For example, thirteen requests per second or block for 1 minute. But it doesn’t work for all types of APIs. For instance, it’s inefficient to rate-limit clients by IP for a wallet management API because the client backend API uses a limited range of IPs. They should be limited by the Access Key, not the IP.

The rate-limiting rule might differ per Access Key. Assume the clients use the wallet management API to generate payment addresses for their shopping websites. The clients with a high user base call the API at a higher rate. As you see, this rule could be more complicated.

Limit API call rates by IP are more complicated in DDoS attacks because they are distributed. Also, the attackers could generate random bogus Access IDs in the header to put more pressure on the Access Secret query. To mitigate this attack, you need a more complicated rate-limiting like a mix of client IP and Access ID. It’s obviously an attack where a single IP sends many requests with 100 different access IDs.

The rate-limiting on its own doesn’t help to mitigate DDoS attacks, and you should consider a combination of rate-limiting with other solutions.

Locking in IP

Each wallet can be locked to the service IP or origin domain. This part of the request can be fooled easily, and the attacker must first have the client’s server IP. But we want to slow down the attack and make it more challenging.

It’s an endless war between API providers and attackers. The attackers don't always target the security. They could affect your service quality by increasing the system load or making more costs. Thus, never trust the client, avoid points of failure, and always have a backup plan.

--

--

Sajjad Rad

Currently a software engineer, always an adventurer