Webhook
Introduction
Inswitch will include two additional HTTP Headers in each callback event: X-Timestamp and X-Signature. These will allow verifying that the events were sent by Inswitch, not by a third party.
The format used for this headers will be:
- X-Timestamp: A timestamp in RFC3339Micro format. Example: 2020-01-01T06:43:33.219225Z
- X-Signature: An RSA signature encoded in base64
- X-SaltLength: An integer representing the salt length for the signature
Example Headers for a typical callback:
connection: close
accept-encoding: gzip
x-timestamp: 2022-05-17T06:43:33.219225Z
x-signature: aBpXf0vFaXbqTP/OhBd533tLAekSGAcYKp2zHu9WCcPLhBPn3pQeQiRkJtXAE2h/kavxv6eROKV+UuKN5PDtxyTgbzep5EG9Gkat+3X03GBqe8NZcpRPxke7KQ0yIzGz5s6SlBD6U+QOA9ZDhhaQbpQy47PkU3eLbIwuTvSdthLCsYSY/4115SF9GGwuZX8MOfvo1ooPXhiYD4Ol+M4YenywHr8wtAJdVOXADOJ1rquslKEv/jjKeOYaO16SP9eYh/QinQC+4LEnUGhOwtUx182Awfhw8XdIRyCujWp8L6r94rGVpb5BSd1/HcnVCUCUbmaqM3xuKvf+P588f6Aaow==
x-saltlength: 20
x-correlationid: b17ce357-5d03-4685-a81c-dd2cf2a87561
content-type: application/json
content-length: 1709
Signature Verification
In order to verify a signature these steps must be followed:
- Obtain the security headers (x-timestamp, x-saltlength, x-signature)
- Recreate the signed payload. The signed payload is created by concatenating:
- A space-trimmed version of the event’s payload (HTTP Body)
- The character -
- The contents of the x-timestamp header
- Compute a hash with the SHA512 function. Use the signed payload string as the message.
- Verify the signature using the PSS padding scheme (may require decoding base64, depending on the libraries used) with the generated hash, the provided public key and the salt length.
Example: Signature verification in Go
// Inputs
xtimestamp := "2022-05-17T03:32:25.287148Z"
xsaltlength := 20
payload := "A message that can be verified"
signatureBase64 := "ADMC3HxP1MkkMhvnESF15MR2cr43ARCFWSgMbfYIYn/0tlBDGYFZKTjt+QpKeykGrtjE0AwVa6baxyUCRFoyJJW8XRR7uKy/D67xSVcQz9Ws9fcfxVGraNEDitYEKWWXuG/KQFv3Y7gklF0P6P5I+eEtkHbDd+IUxfz5eSbR+L/uV7BkDVWoPfF9zdOZ22P4efLxGPQBD/1LGa0hCxngU55da3/fUmO6vohUu0UtIDow4r+Jz9fnfPFRqb+ZtRCtXLxKpmubxihjV146+4TR6eooiXyvt8U005L/4pfn0rvdxRPIvnpyMwbJ+tcqo4YpfQBnp8OfGPcxcD91qaWpXA=="
// Recreate the signed payload
signedPayload := payload + "-" + xtimestamp
// Read public key from file
b, err := ioutil.ReadFile("inshub.pem")
if err != nil {
fmt.Println(err)
return
}
block, _ := pem.Decode(b)
parseResult, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
fmt.Println(err)
return
}
publicKey := parseResult.(*rsa.PublicKey)
// End: Read public key from file
//Decode the base64 signature if necessary
signature, _ := base64.StdEncoding.DecodeString(signatureBase64)
// Get signedPayload as byte array
msg := []byte(signedPayload)
// Before verifying, we need to hash our message
// The hash is what is actually signed
msgHash := sha512.New()
_, err = msgHash.Write(msg)
if err != nil {
panic(err)
}
msgHashSum := msgHash.Sum(nil)
// To verify the signature, we provide the public key, the salt length,
// the hashing algorithm, the hash sum of our message and the
// signature (in this case as binary)
options := &rsa.PSSOptions{SaltLength: xsaltlength}
err = rsa.VerifyPSS(publicKey, crypto.SHA512, msgHashSum, signature, options)
if err != nil {
fmt.Println("could not verify signature: ", err)
return
}
// If we don't get any error from the `VerifyPSS` method, that means our
// signature is valid
fmt.Println("signature verified")
Example: Signature verification in NodeJS
/**
* @function isValidCallbackRequest
*
* @description Validate HUB Callback Payload.
*
* @param {string} payload - Stringified request payload
* @param {string} timestamp - x-timestamp header
* @param {string} signature - x-signature header
* @param {number} saltLength - x-saltlength header
*
* @returns {Promise<boolean>} validation result (true/false)
*
*/
export async function isValidCallbackRequest(
payload: string,
timestamp: string,
signature: string,
saltLength: number,
): Promise<boolean> {
if (!process.env.INS_HUB_PUBLIC_KEY_PATH) {
throw new Error("Configuración incompleta");
}
// Recreate the signed payload
const signedPayload = ${payload.trim()}-${timestamp.trim()};
// Read public key from file
const publicPem: Buffer = await getPublicKey();
// Get signedPayload as a buffer
const messageBuffer = Buffer.from(new TextEncoder().encode(signedPayload));
// To verify the signature, we provide the public key, the salt length,
// the hashing algorithm, the hash sum of our message and the signature
const verifier = createVerify("sha512");
verifier.update(messageBuffer);
return verifier.verify(
{
key: publicPem,
saltLength,
padding: constants.RSA_PKCS1_PSS_PADDING,
},
Buffer.from(signature, "base64"),
);
}
Updated about 1 year ago