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:

  1. Obtain the security headers (x-timestamp, x-saltlength, x-signature)
  2. Recreate the signed payload. The signed payload is created by concatenating:
    1. A space-trimmed version of the event’s payload (HTTP Body)
    2. The character -
    3. The contents of the x-timestamp header
  3. Compute a hash with the SHA512 function. Use the signed payload string as the message.
  4. 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"),
  );
}