Recently, I had the task of researching the integration of App Store Server Notifications, which is a type of webhook, to receive notifications from Apple on my server. During the integration process, I discovered some interesting details that I think would be helpful to share. If you're not familiar with what a webhook is, you can refer to the article What is a Webhook? When to Use Webhooks?.
We all know about making purchases within applications. When a purchase is successfully made, Apple sends a notification to our server, which contains various information about the order such as the name, purchase date, and status. This information serves as a basis for further processing the customer's order. Since a successful purchase action in the application only occurs when the user has successfully made the purchase, only Apple knows if the purchase was successful or not because they handle the payment process for us. Then, a notification is sent to an API (webhook) that we have set up in advance, indicating the result of the successful or failed payment process.
During my work, I discovered that Apple has a way to protect the data they send to our server in a very "trustworthy" manner. It took me some time to research this, and it inspired me to compile some of the security methods for webhooks that have been accumulated over time.
Note that there are still many other methods that are not mentioned here. I'm simply summarizing some common or well-known methods. So, if you know any other methods, please leave the information in the comments section below the article.
When providing an API endpoint to receive data, if unfortunate circumstances arise and an attacker or curious person discovers it, they may intentionally exploit it by sending incorrect information to the server, which can lead to various risks. Therefore, the most effective method is to keep the API secret and always verify the received data to ensure that it originates from the integrated service.
For example, if you provide an endpoint /webhook/serviceA
to receive data from serviceA
, you need to ensure that the received data actually comes from serviceA
.
The first prerequisite is to require HTTPS, which means that serviceA
will refuse to send data if the endpoint does not support HTTPS.
Nowadays, HTTPS is gradually replacing traditional HTTP because it offers higher reliability and security. Data is encrypted during transmission, limiting Man-in-the-middle attacks.
For this reason, HTTPS has become a mandatory requirement for transmitting data through API calls, which are the mechanism for sending/receiving data in webhooks.
The best way to know that serviceA
is the sender is to ensure that the data is sent from its correct IP address. serviceA
may need to provide a list of its owned IP addresses, which will be used to query our endpoint. Our task is to authenticate whether the received IP address is within the trusted list. If not, there is a high chance that we are being attacked.
This method is fast and effective, but it needs to be supported by serviceA
because they have to provide all the IP addresses. However, in the current era of distributed technology and complex information systems, IP addresses can be constantly changed, making implementation complex and introducing risks during operation.
Imagine a scenario where serviceA
adds a new IP address to the list that our system has not yet updated. What will happen then?
"Choose a secret key, enter it on our management page, and we will send it along with the endpoint data to you" - this is the motto of this method.
Since only the two parties know the secret key, not providing the correct secret key can be considered an attack from another party. The secret key is usually sent back through HTTP headers. Our task is to extract it and compare it to see if it matches.
This method is easy to implement and has a certain level of reliability. However, if the secret key is unfortunately leaked... well, you know what happens. Because the key is usually in plain text, not encrypted, and needs to be stored somewhere, its reliability decreases.
In this category, Basic Auth is also a form of secret key. It provides a username and password for serviceA
to authenticate before making a call to the endpoint.
The aforementioned methods still have a weakness: the data is not properly encrypted, or in other words, there is no way to know if the data sent to our server guarantees integrity. The data transmission process is not as simple as between serviceA
and the endpoint; it passes through many points before reaching the destination. If the data is modified somewhere, how can we detect it?
If you have worked with JSON Web Tokens (JWT), you will know the mechanism of protecting data integrity by asymmetric encryption. With the base64-encoded structure of the three parts of a JWT, the header contains instructions about the encryption algorithm, the payload contains the data, and the last part is a string generated by asymmetrically encrypting the data with a secret key. To authenticate, we only need to use the public key to check if the data has been modified after being signed, because any change, no matter how small, in the payload will cause the signature to be invalid.
This method is more complex but provides a higher level of reliability. The secret key needs to be kept securely to sign the data before sending it. The secret key is also encrypted, so the risk of attack is lower compared to plain text.
In essence, Apple also chooses asymmetric encryption to protect the integrity of the data they send, but with a slightly higher level of complexity.
First, the data is encoded in base64 to increase the "danger" factor, as this encoding adds some difficulty to quickly eavesdrop on and requires decryption to know the actual content.
Apple signs on some data objects in the form of JWS to ensure the integrity of the data before using it. The process can be summarized in 2 steps:
Therefore, when decoding the headers, you will see the signature algorithm, along with an x5c object like this:
{
"alg": "ES256",
"x5c": [
"MIIEMDCCA...",
"MIIDFjCCA...",
"MIICQzCCA..."
]
}
At that time, the public key used to verify the data is x5c[0]
. But what is the significance of x5c[1]
and x5c[2]
? We know that the asymmetric encryption algorithm based on ES256
is difficult to crack, so using the public key to ensure the integrity of the data is almost absolute, as only Apple knows the secret key.
After some research, it turns out that the other two keys are used to authenticate the public key. Yes, that's right, x5c[1]
and x5c[2]
are used to authenticate whether x5c[0]
is really from Apple or not.
So, x5c[2]
is the root service certificate of Apple (Certificate Authority (CA)) that has been trusted, while x5c[1]
is the intermediate certificate and x5c[0]
is the certificate used to verify the signature that Apple has signed for the data.
Trusted CAs are distributed through operating systems, meaning that computers are pre-installed with a number of trusted CA certificates worldwide, including Apple's CA. Some tools like openssl can determine whether a CA is trustworthy or not. Therefore, the authentication flow at this point is to use a tool to verify the CA (x5c[2]
) -> verify the intermediate certificate (x5c[1]
) -> verify the public key (x5c[0]
). If all are valid, we can be sure that the data is sent from Apple.
Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.
Comments (0)