Webhook Signature Verification
Digital signature generated based on asymmetric cryptography
Digital signature generation utilizes pairs of related keys—each consisting of a public key and a corresponding private key—generated through cryptographic algorithms based on one-way functions. The advantage of asymmetric cryptography is that it does not require the private key to be exchanged over a secure channel prior to communication, although it demands more computational resources to generate and verify signatures.
InPost creates the key pairs, distributing the public key as a public certificate. For every webhook call, InPost signs the message hash using its private key and transmits the signature in the x-inpost-signature
header. The receiver must then perform complementary operations to validate the message's integrity and authenticity. InPost employs RSA keys and the SHA256withRSA algorithm for signing.
Verifying the signature
Importing the InPost public certificate to the keystore
The keystore can be used to store the certificate, which allows this certificate to be used in an application to load the public key to verify the webhook signature. To create the empty keystore:
keytool -genkeypair -alias al -keyalg RSA -keysize 2048 -dname "CN=Name" -validity 365 -storetype JKS -keystore test_keystore.jks -storepass testpasswd
keytool -delete -alias al -storepass testpasswd -keystore test_keystore.jks
To import the public certificate to the empty keystore
openssl x509 -outform der -in sandbox-certificate.pem -out sandbox-certificate.der
keytool -import -alias your-alias -keystore test_keystore.jks -file sandbox-certificate.der
Explanation: In the above examples, the keystore with the name test_keystore.jks
contains the InPost public certificate sandbox-certificate.pem
under alias your-alias
. The password to the keystore in the example is testpasswd
.
Loading the InPost Public Certificate from the keystore in your application
The example code block below demonstrates how to load the public certificate from the keystore test_keystore.jks
using the alias and password that has been created.
public PublicKey loadPublicKeyFromKeystore() {
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance("JKS");
keyStore.load(
new FileInputStream("test_keystore.jks"),
"testpasswd".toCharArray());
Certificate certificate = keyStore.getCertificate("your-alias");
return certificate.getPublicKey();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Verifying the signature using the loaded InPost public key
To verify the signature, the following data should be used:
- The body of the webhook message without any modifications, formatting, or conversions applied;
- The signature from the webhook header
x-inpost-signature
. The signature should be used without any modifications, formatting, or conversions applied; and - The Public key loaded from the keystore in your application.
Example 1 : The below example shows the raw content if only the body of the webhook message is signed.
{"customerReference":"2b4f51c2-42c7-415e-a821-d2fd1c5032a2","trackingNumber":"120000018332540090213375","eventId":"fb2ace20-2d8c-41da-94c4-3bd93f1451a1#CRE.1001","eventCode":"CRE.1001","timestamp":"2025-01-08T14:02:55.374675Z","location":null,"delivery":{"recipientName":null,"deliveryNotes":null},"shipment":{"type":null}}
Example 2 : The below example shows the raw content if the timestamp header is also included into the content.
2025-01-08T14:03:55.387Z.{"customerReference":"2b4f51c2-42c7-415e-a821-d2fd1c5032a2","trackingNumber":"120000018332540090213375","eventId":"fb2ace20-2d8c-41da-94c4-3bd93f1451a1#CRE.1001","eventCode":"CRE.1001","timestamp":"2025-01-08T14:02:55.374675Z","location":null,"delivery":{"recipientName":null,"deliveryNotes":null},"shipment":{"type":null}}
Example of Signed Content :
As a result, the below example shows the body and signature of the body of a message created using the private key corresponding to the InPost public certificate for sandbox environment. (The public certificate can be found below as sandbox-certificate.pem
).
private static final String BODY = "{\"customerReference\":\"9000010305\",\"trackingNumber\":\"120000018332540090213375\",\"eventId\":\"76b2262a-4274-40a1-8aac-89b048974d75#MMD.1001\",\"eventCode\":\"MMD.1001\",\"timestamp\":\"2024-11-25T16:51:40.111968200Z\",\"location\":{\"id\":null,\"address\":null,\"city\":null,\"country\":\"PL\",\"name\":null,\"postalCode\":null,\"type\":\"LOGISTIC_CENTER\",\"description\":null},\"delivery\":{\"recipientName\":null,\"deliveryNotes\":null},\"shipment\":{\"type\":null}}";
private static final String BASE64_ENCODED_DIGITAL_SIGNATURE = "KoTi4cs4diR380hDIIPOmgc4gLPB/xQisqa2vx0qcbNqJpysGThZ951/sogqKsOnK7zG8Vzh5KgrQlBzThoalsnDYEwJmuVsgf4DhIpxZRzLSWv8oW9+qYLrWtVGGoOx6Uow2AmaAxuV9xt3VmS8FPSM0OKQD8wDLdax/iWO2GM6tE0XWX/H6iIKaQmdVAh+fI3OTKxxgdzXn6nn0YNPTUt7GXxBuJ5pcTBEN7ne72LdIfJtJtX4S+I4I6rVOfPqm2RtX6lQX9ODHoJ6xke14NOwvovCpaKIRB3ktpS1YP4KM7ze+sRdmT2JKRUAce3qiPHemHnGt1vQa3tDN0dsm6uuI0Z9Zq16y9P/vfx58NW8dTgOK8lLRTY06nbNTEvopWT8Oe8JNGMVXWeoeQknpi/qcOKpcA0Oj8rAwq+oHEcHhQCqTGB1jCQNwPvHNSC2nysXvB1qFjge2oW0EvLY1IC4qk0Qqx48l0jdgSZUXqWeldxwnBUwaecrZHP1+sTgtMOWi8zJhofpawc2NuuUgcDDEXEOglYoAZCImXhN1du0tJ9rmMj4xhhIudLr+4Tf5En9k852EBWDvXtSbpxGMFVp1NBYOpfuNmLv30JHrvY40ivnd+Tz2fNlBDpH8yohXtiwHYpMT2px8+L0RPvpN2/O3Bl5Ak166bhWpJhCGNo=";
Example code : This method verifies the signature using the loaded InPost public key.
public boolean verifySignatureUsingPublicKeyFromKeystore() {
var publicKey = loadPublicKeyFromKeystore();
return verifySignature(BODY, publicKey, BASE64_ENCODED_DIGITAL_SIGNATURE);
}
public boolean verifySignature(String body, PublicKey publicKey, String base64DigitalSignature) {
try {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(body.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.getDecoder().decode(base64DigitalSignature));
} catch (Exception e) {
log.error("Exception during signature verification", e);
}
return false;
}
Example Source Code
DigitalSignatureVerifierExample.java
Public Certificates
sandbox-certificate.pem
production-certificate.pem
HMAC signature generated using shared secret key
HMAC, which stands for Hash-Based Message Authentication Code or Keyed-Hash Message Authentication Code, uses a cryptographic hash function and a secret cryptographic key. Unlike asymmetric cryptography, HMAC authentication relies on a shared secret, eliminating the need for a complex public key infrastructure. This method requires that communicating parties establish a trusted channel to exchange the secret key prior to communication.
Example HMAC key : fdXbfU27DBNG6LuoHu@ThKl3
InPost uses the HMAC-SHA256 algorithm and secret keys provided by clients to generate a signature of the message hash. This signature is included in the x-inpost-signature header
. Upon receiving a message, the receiver computes the HMAC to verify the integrity and authenticity of the message. If the computed HMAC matches the one sent by InPost, the message content is confirmed as untampered and trustworthy.
Verifying the signature using a HMAC shared secret key
The verification process entails calculating the signature based on the body of the webhook message, and then comparing this signature sent by the sender in the x-inpost-signature
header.
To verify the signature, the following data should be used:
- The body of the webhook message without any modifications, formatting, or conversions applied;
- The signature from the webhook header
x-inpost-signature
. The signature should be used without any modifications, formatting, or conversions applied; and - The HMAC shared secret key used to encode & decode the message.
Example 1 : The below example shows the raw content if only the body of the webhook message is signed.
{"customerReference":"2b4f51c2-42c7-415e-a821-d2fd1c5032a2","trackingNumber":"120000018332540090213375","eventId":"fb2ace20-2d8c-41da-94c4-3bd93f1451a1#CRE.1001","eventCode":"CRE.1001","timestamp":"2025-01-08T14:02:55.374675Z","location":null,"delivery":{"recipientName":null,"deliveryNotes":null},"shipment":{"type":null}}
Example 2 : The below example shows the raw content if the timestamp header is also included into the content.
2025-01-08T14:03:55.387Z.{"customerReference":"2b4f51c2-42c7-415e-a821-d2fd1c5032a2","trackingNumber":"120000018332540090213375","eventId":"fb2ace20-2d8c-41da-94c4-3bd93f1451a1#CRE.1001","eventCode":"CRE.1001","timestamp":"2025-01-08T14:02:55.374675Z","location":null,"delivery":{"recipientName":null,"deliveryNotes":null},"shipment":{"type":null}}
Example of Signed Content : As a result, the below example shows the body and signature of the body of a message created using the HMAC shared secret key.
private static final String BODY = "{\"customerReference\":\"9000010305\",\"trackingNumber\":\"120000018332540090213375\",\"eventId\":\"76b2262a-4274-40a1-8aac-89b048974d75#MMD.1001\",\"eventCode\":\"MMD.1001\",\"timestamp\":\"2024-11-25T16:51:40.111968200Z\",\"location\":{\"id\":null,\"address\":null,\"city\":null,\"country\":\"PL\",\"name\":null,\"postalCode\":null,\"type\":\"LOGISTIC_CENTER\",\"description\":null},\"delivery\":{\"recipientName\":null,\"deliveryNotes\":null},\"shipment\":{\"type\":null}}";
private static final String BASE64_ENCODED_DIGITAL_SIGNATURE = "i1PzFQMpGoM3YwjcDUEtBwNMCa01kjCykHLLoA5oZtE=";
Example code : This method verifies the signature using the HMAC shared secret key.
public boolean verifySignature(String body, String signatureFromXInPostSignatureHeader) {
String newSignature = createSignature(body);
return signatureFromXInPostSignatureHeader.equals(newSignature);
}
public String createSignature(String body) {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm);
Mac mac = Mac.getInstance(algorithm);
mac.init(secretKeySpec);
byte[] contentToSign = body.getBytes(StandardCharsets.UTF_8);
return Base64.getEncoder().encodeToString((mac.doFinal(contentToSign)));
} catch (Exception e) {
log.error("Exception during signature verification", e);
}
return null;
}